Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions spec/System/TestCommon_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe("Common", function()
function ParentClass:ConstructorTestParentClass()
return self
end
local ChildClass = newClass("ConstructorTestProblemChildClass", "ConstructorTestParentClass")
local ChildClass = newClass("ConstructorTestProblemChild", "ConstructorTestParentClass")
function ChildClass:ConstructorTestProblemChild()
-- Intentionally does not call self:ConstructorTestParentClass()
return self
Expand Down Expand Up @@ -40,7 +40,7 @@ describe("Common", function()
return self
end

local ChildClass = newClass("ConstructorTestProblemChildClass", "ConstructorTestParentClass")
local ChildClass = newClass("ConstructorTestProblemChild", "ConstructorTestParentClass")
function ChildClass:ConstructorTestProblemChild()
self.ConstructorTestParentClass()
return self
Expand Down
4 changes: 2 additions & 2 deletions spec/System/TestItemMods_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -728,8 +728,8 @@ describe("TetsItemMods", function()
end,
})

local attributeModList = calcs.buildModListForNode(env, attributeNode, 0, false)
local smallModList = calcs.buildModListForNode(env, smallNode, 0, false)
local attributeModList = calcs.buildModListForNode(env, attributeNode, nil, 0, false)
local smallModList = calcs.buildModListForNode(env, smallNode, nil, 0, false)
GlobalCache.cachedData[envMode] = nil

assert.are.equals(7, attributeModList:Sum("BASE", nil, "Str"))
Expand Down
26 changes: 25 additions & 1 deletion spec/System/TestPassiveSpec_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,13 @@ Item Level: 80
return node
end

it("rebuilds an allocated jewel socket's distance from the class start", function()
local socket = build.spec.nodes[60735]
build.spec:AllocNode(socket)

assert.True(socket.distanceToClassStart > 0)
end)

it("normal passive allocation promotes the shortest path instead of using a longer detour", function()
local spec = build.spec
allocNode(spec, 56651, 0)
Expand All @@ -564,7 +571,7 @@ Item Level: 80
assert.are.equals(0, weaponSetNode.allocMode)
end)

it("normal passive allocation promotes the weapon-set chain behind the path root", function()
it("normal passive allocation preserves an unused weapon-set path", function()
local spec = build.spec
allocNode(spec, 56651, 0)
allocNode(spec, 35324, 0)
Expand All @@ -577,6 +584,23 @@ Item Level: 80
spec.allocMode = 0
spec:AllocNode(promotedNode)

assert.True(promotedNode.alloc)
assert.are.equals(0, promotedNode.allocMode)
assert.are.equals(1, spec.nodes[18548].allocMode)
assert.are.equals(1, spec.nodes[35660].allocMode)
end)

it("normal passive allocation promotes a required weapon-set path", function()
local spec = build.spec
allocNode(spec, 35660, 1)
allocNode(spec, 18548, 1)

local promotedNode = spec.nodes[28992]
assert.are.equals("Honed Instincts", promotedNode.dn)

spec.allocMode = 0
spec:AllocNode(promotedNode)

assert.True(promotedNode.alloc)
assert.are.equals(0, promotedNode.allocMode)
assert.are.equals(0, spec.nodes[18548].allocMode)
Expand Down
15 changes: 12 additions & 3 deletions src/Classes/ModDB.lua
Original file line number Diff line number Diff line change
Expand Up @@ -134,14 +134,17 @@ end

function ModDBClass:SumInternal(context, modType, cfg, flags, keywordFlags, source, ...)
local result = 0
local globalLimits = { }
local globalLimits
for i = 1, select('#', ...) do
local modList = self.mods[select(i, ...)]
if modList then
for i = 1, #modList do
local mod = modList[i]
if mod.type == modType and band(flags, mod.flags) == mod.flags and MatchKeywordFlags(keywordFlags, mod.keywordFlags) and (not source or ( mod.source and (mod.source:match("[^:]+") == source or mod.source == source))) then
if mod[1] then
if not globalLimits then
globalLimits = {}
end
local value = context:EvalMod(mod, cfg, globalLimits) or 0
result = result + value
else
Expand All @@ -160,7 +163,7 @@ end
function ModDBClass:MoreInternal(context, cfg, flags, keywordFlags, source, ...)
local result = 1
local modPrecision = nil
local globalLimits = { }
local globalLimits
for i = 1, select('#', ...) do
local modList = self.mods[select(i, ...)]
local modResult = 1 --The more multipliers for each mod are computed to the nearest percent then applied.
Expand All @@ -170,6 +173,9 @@ function ModDBClass:MoreInternal(context, cfg, flags, keywordFlags, source, ...)
if mod.type == "MORE" and band(flags, mod.flags) == mod.flags and MatchKeywordFlags(keywordFlags, mod.keywordFlags) and (not source or mod.source:match("[^:]+") == source) then
local value
if mod[1] then
if not globalLimits then
globalLimits = {}
end
value = context:EvalMod(mod, cfg, globalLimits) or 0
else
value = mod.value or 0
Expand Down Expand Up @@ -270,7 +276,7 @@ function ModDBClass:ListInternal(context, result, cfg, flags, keywordFlags, sour
end

function ModDBClass:TabulateInternal(context, result, modType, cfg, flags, keywordFlags, source, ...)
local globalLimits = { }
local globalLimits
for i = 1, select('#', ...) do
local modName = select(i, ...)
local modList = self.mods[modName]
Expand All @@ -280,6 +286,9 @@ function ModDBClass:TabulateInternal(context, result, modType, cfg, flags, keywo
if (mod.type == modType or not modType) and band(flags, mod.flags) == mod.flags and MatchKeywordFlags(keywordFlags, mod.keywordFlags) and (not source or mod.source:match("[^:]+") == source) then
local value
if mod[1] then
if not globalLimits then
globalLimits = {}
end
value = context:EvalMod(mod, cfg, globalLimits)
else
value = mod.value
Expand Down
186 changes: 101 additions & 85 deletions src/Classes/PassiveSpec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1057,24 +1057,28 @@ function PassiveSpecClass:FindStartFromNode(node, visited, noAscend, allocMode,
node.visited = true
t_insert(visited, node)
-- For each node which is connected to this one, check if...
local nodeAscendancy = node.ascendancyName
for _, other in ipairs(node.linked) do
-- Either:
-- - the other node is a start node, or
-- - there is a path to a start node through the other node which didn't pass through any nodes which have already been visited
local startIndex = #visited + 1
local startIndex = nodeAscendancy and #visited + 1
local otherAlloc = other.alloc or (alternateClassStartNodes and alternateClassStartNodes[other.id])
if otherAlloc and self:CanPathThroughAllocMode(allocMode, other) and
(other.type == "ClassStart" or other.type == "AscendClassStart" or
(not other.visited and node.type ~= "Mastery" and self:FindStartFromNode(other, visited, noAscend, allocMode, alternateClassStartNodes))
) then
if node.ascendancyName and not other.ascendancyName then
-- Pathing out of Ascendant, un-visit the outside nodes
for i = startIndex, #visited do
visited[i].visited = false
visited[i] = nil
if otherAlloc and self:CanPathThroughAllocMode(allocMode, other) then
local otherType = other.type
if
(otherType == "ClassStart" or otherType == "AscendClassStart" or
(not other.visited and node.type ~= "Mastery" and self:FindStartFromNode(other, visited, noAscend, allocMode, alternateClassStartNodes))
) then
if nodeAscendancy and not other.ascendancyName then
-- Pathing out of Ascendant, un-visit the outside nodes
for i = startIndex, #visited do
visited[i].visited = false
visited[i] = nil
end
elseif not noAscend or otherType ~= "AscendClassStart" then
return true
end
elseif not noAscend or other.type ~= "AscendClassStart" then
return true
end
end
end
Expand Down Expand Up @@ -1263,72 +1267,6 @@ function PassiveSpecClass:CollectGrantedPassiveNodesFromItems(itemsTab, baseAllo
return granted
end

-- Perform a breadth-first search of the tree, starting from this node, and determine if it is the closest node to any other nodes
function PassiveSpecClass:BuildPathFromNode(root)
root.pathDist = 0
root.path = { }
root.pathRoot = root
local queue = { root }
local o, i = 1, 2 -- Out, in
while o < i do
-- Nodes are processed in a queue, until there are no nodes left
-- All nodes that are 1 node away from the root will be processed first, then all nodes that are 2 nodes away, etc
local node = queue[o]
o = o + 1

if node.unlockConstraint then
for _, nodeId in ipairs(node.unlockConstraint.nodes) do
if not self.nodes[nodeId].alloc then
goto continue
end
end
end
local curDist = node.pathDist
-- Iterate through all nodes that are connected to this one
for _, other in ipairs(node.linked) do
-- Paths must obey these rules:
-- 1. They must not pass through class or ascendancy class start nodes (but they can start from such nodes)
-- 2. They cannot pass between different ascendancy classes or between an ascendancy class and the main tree
-- The one exception to that rule is that a path may start from an ascendancy node and pass into the main tree
-- This permits pathing from the Ascendant 'Path of the X' nodes into the respective class start areas
-- 3. They must not pass away from mastery nodes
-- 4. Unlock constraints must be satisfied

-- validate if the other node have unlockConstraints met
local canPath = true
if other.unlockConstraint then
for _, nodeId in ipairs(other.unlockConstraint.nodes) do
if not self.nodes[nodeId].alloc then
canPath = false
break
end
end
end

if not other.pathDist then
ConPrintTable(other, true)
end
if node.type ~= "Mastery" and other.type ~= "ClassStart" and other.type ~= "AscendClassStart" and (not other.alloc or self:CanPathThroughAllocMode(root.allocMode or 0, other)) and other.pathDist > curDist and (node.ascendancyName == other.ascendancyName or (curDist == 0 and not other.ascendancyName)) and canPath then
-- The shortest path to the other node is through the current node
other.pathDist = curDist
if not other.alloc then
other.pathDist = other.pathDist + 1
end
other.path = wipeTable(other.path)
other.pathRoot = root
other.path[1] = other
for i, n in ipairs(node.path) do
other.path[i+1] = n
end
-- Add the other node to the end of the queue
queue[i] = other
i = i + 1
end
end
::continue::
end
end

-- Determine this node's distance from the class' start
-- Only allocated nodes can be traversed
function PassiveSpecClass:SetNodeDistanceToClassStart(root)
Expand Down Expand Up @@ -1422,6 +1360,78 @@ function PassiveSpecClass:NodesInIntuitiveLeapLikeRadius(node)
return result
end

-- Multi-source 0-1 BFS to find what other root (i.e., allocated) nodes each node is closest to
---@param roots Node[] A list of currently allocated, and other nodes which should be considered as the sources of distances.
function PassiveSpecClass:BuildNodePathsToRootNodes(roots)
-- A dequeue. We will keep a pointer to the start and end of this to keep
-- track of its length
local q = {}
for _, node in ipairs(roots) do
node.pathDist = 0
node.path = wipeTable(node.path)
node.pathRoot = node
t_insert(q, node)
end
local qStart = 1
local qLen = #q
while qStart <= qLen do
-- pop front
local node = q[qStart]
qStart = qStart + 1
if node.unlockConstraint then
for _, nodeId in ipairs(node.unlockConstraint.nodes) do
if not self.nodes[nodeId].alloc then
goto continueBuildPath
end
end
end
local linked = node.linked
local nodeDist = node.pathDist
local nodePath = node.path
for i = 1, #linked do
local other = linked[i]
local weight = other.alloc and 0 or 1
local distViaNode = nodeDist + weight
local otherDist = other.pathDist or math.huge
local preferNormalRoot = distViaNode == otherDist and (node.pathRoot.allocMode or 0) == 0 and other.pathRoot and (other.pathRoot.allocMode or 0) ~= 0

-- validate if the other node have unlockConstraints met
local canPath = true
if other.unlockConstraint then
for _, nodeId in ipairs(other.unlockConstraint.nodes) do
if not self.nodes[nodeId].alloc then
canPath = false
break
end
end
end

if (distViaNode < otherDist or preferNormalRoot)
and node.type ~= "Mastery" and other.type ~= "ClassStart" and other.type ~= "AscendClassStart" and (not other.alloc or self:CanPathThroughAllocMode(node.allocMode or 0, other)) and (node.ascendancyName == other.ascendancyName or (nodeDist == 0 and not other.ascendancyName)) and canPath then
-- if this node is free, push it to the front so that it can shorten paths
if weight == 0 then
qStart = qStart - 1
q[qStart] = other
-- otherwise push to back
else
qLen = qLen + 1
q[qLen] = other
end

-- save path and distance for the node
other.pathDist = distViaNode
local path = wipeTable(other.path)
path[1] = other
for i = 1, #nodePath do
path[i + 1] = nodePath[i]
end
other.path = path
other.pathRoot = node.pathRoot
end
end
::continueBuildPath::
end
end
-- Rebuilds dependencies and paths for all nodes
function PassiveSpecClass:BuildAllDependsAndPaths()
-- This table will keep track of which nodes have been visited during each path-finding attempt
Expand Down Expand Up @@ -1460,7 +1470,7 @@ function PassiveSpecClass:BuildAllDependsAndPaths()
self.switchableNodes = { }
for id, node in pairs(self.nodes) do
node.depends = wipeTable(node.depends)
node.intuitiveLeapLikesAffecting = { }
node.intuitiveLeapLikesAffecting = wipeTable(node.intuitiveLeapLikesAffecting)
node.conqueredBy = nil

-- ignore cluster jewel nodes that don't have an id in the tree
Expand Down Expand Up @@ -2009,17 +2019,23 @@ function PassiveSpecClass:BuildAllDependsAndPaths()
node.distanceToClassStart = 0
end
end
for id, node in pairs(self.allocNodes) do
local rootList = {}
for _, node in pairs(self.allocNodes) do
if #node.intuitiveLeapLikesAffecting == 0 or node.connectedToStart then
self:BuildPathFromNode(node)
if node.isJewelSocket or node.expansionJewel then
self:SetNodeDistanceToClassStart(node)
end
t_insert(rootList, node)
end
end
self:BuildNodePathsToRootNodes(rootList)
for _, node in ipairs(rootList) do
if node.isJewelSocket or node.expansionJewel then
self:SetNodeDistanceToClassStart(node)
end
end
local alternateClassStartNodesArray = {}
for _, node in pairs(alternateClassStartNodes) do
self:BuildPathFromNode(node)
alternateClassStartNodesArray[#alternateClassStartNodesArray + 1] = node
end
self:BuildNodePathsToRootNodes(alternateClassStartNodesArray)
end

function PassiveSpecClass:ReplaceNode(old, newNode)
Expand Down
Loading
Loading