diff --git a/src/Classes/ModList.lua b/src/Classes/ModList.lua index 7b29f3aec..00ae37856 100644 --- a/src/Classes/ModList.lua +++ b/src/Classes/ModList.lua @@ -217,6 +217,32 @@ function ModListClass:TabulateInternal(context, result, modType, cfg, flags, key end end +---HasModInternal +--- Checks if a mod exists with the given properties +---@param modType string @The type of the mod, e.g. "BASE" +---@param flags number @The mod flags to match +---@param keywordFlags number @The mod keyword flags to match +---@param source string @The mod source to match +---@return boolean @true if the mod is found, false otherwise. +function ModListClass:HasModInternal(modType, flags, keywordFlags, source, ...) + for i = 1, select('#', ...) do + local modName = select(i, ...) + for i = 1, #self do + local mod = self[i] + if mod.name == modName and mod.type == modType and band(flags, mod.flags) == mod.flags and MatchKeywordFlags(keywordFlags, mod.keywordFlags) and (not source or mod.source:match("[^:]+") == source) then + return true + end + end + end + if self.parent then + local parentResult = self.parent:HasModInternal(modType, flags, keywordFlags, source, ...) + if parentResult == true then + return true + end + end + return false +end + function ModListClass:Print() for _, mod in ipairs(self) do ConPrintf("%s|%s", modLib.formatMod(mod), mod.source or "?") diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index 2b76120a2..294782679 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -64,6 +64,10 @@ function PassiveSpecClass:Init(treeVersion, convert) end end + -- Table of intuitive leap-like jewel node modifiers from one type of node to others with the given radius + -- { from = nodeType, radiusIndex = index, to = { nodeType, [...] } } + self.intuitiveLeapLikeNodes = { } + -- List of currently allocated nodes -- Keys are node IDs, values are nodes self.allocNodes = { } @@ -1103,6 +1107,23 @@ function PassiveSpecClass:BuildAllDependsAndPaths() -- This table will keep track of which nodes have been visited during each path-finding attempt local visited = { } local attributes = { "Dexterity", "Intelligence", "Strength", "Attribute" } + + -- First check for mods that affect intuitive leap-like properties of other nodes + local processed = { } + local intuitiveLeapLikeNodes = self.intuitiveLeapLikeNodes + wipeTable(intuitiveLeapLikeNodes) + for id, node in pairs(self.allocNodes) do + if node.ascendancyName then -- avoid processing potentially replaceable nodes + self.tree:ProcessStats(node) + if node.modList:HasMod("LIST", nil, "AllocateFromNodeRadius") then + for _, radius in ipairs(node.modList:List(nil, "AllocateFromNodeRadius")) do + t_insert(intuitiveLeapLikeNodes, radius) + end + end + processed[id] = true + end + end + -- Check all nodes for other nodes which depend on them (i.e. are only connected to the tree through that node) self.switchableNodes = { } for id, node in pairs(self.nodes) do @@ -1155,6 +1176,32 @@ function PassiveSpecClass:BuildAllDependsAndPaths() end end end + for _, intuitiveLeapMap in ipairs(intuitiveLeapLikeNodes) do + local allocatable = false + for _, allocatableNodeType in ipairs(intuitiveLeapMap.to) do + if allocatableNodeType == node.type then + allocatable = true + break + end + end + if allocatable then + if intuitiveLeapMap.from == "Keystone" then + for keyName, keyNode in pairs(self.tree.keystoneMap) do + if self.allocNodes[keyNode.id] and keyNode.nodesInRadius and keyNode.nodesInRadius[intuitiveLeapMap.radiusIndex][id] then + t_insert(node.intuitiveLeapLikesAffecting, self.nodes[keyNode.id]) + end + end + end + -- We don't keep a `nodesInRadius` map for notables, so this is disabled for now + -- if intuitiveLeapMap.from == "Notable" then + -- for keyName, keyNode in pairs(self.tree.notableMap) do + -- if keyNode.nodesInRadius[intuitiveLeapMap.radiusIndex][node.id] then + -- t_insert(node.intuitiveLeapLikesAffecting, self.nodes[nodeId]) + -- end + -- end + -- end + end + end end if node.alloc then node.depends[1] = node -- All nodes depend on themselves @@ -1472,6 +1519,35 @@ function PassiveSpecClass:BuildAllDependsAndPaths() -- If n is a jewel socket containing an intuitive leap-like jewel, nodes in its radius (or the radius of the keystone) -- may be dependent on this node if they're found to be unconnected to the start + for _, intuitiveLeapMap in ipairs(intuitiveLeapLikeNodes) do + local allocatable = false + for _, allocatableNodeType in ipairs(intuitiveLeapMap.to) do + if allocatableNodeType == node.type then + allocatable = true + break + end + end + if allocatable then + if intuitiveLeapMap.from == n.type then + for affectedNodeId, affectedNode in pairs(n.nodesInRadius[intuitiveLeapMap.radiusIndex]) do + if affectedNode.alloc then + if not intuitiveLeaps[node.id] then + intuitiveLeaps[node.id] = { } + end + t_insert(intuitiveLeaps[node.id], affectedNode) + end + end + end + -- We don't keep a `nodesInRadius` map for notables, so this is disabled for now + -- if intuitiveLeapMap.from == "Notable" then + -- for keyName, keyNode in pairs(self.tree.notableMap) do + -- if keyNode.nodesInRadius[intuitiveLeapMap.radiusIndex][node.id] then + -- t_insert(node.intuitiveLeapLikesAffecting, self.nodes[nodeId]) + -- end + -- end + -- end + end + end if not intuitiveLeaps[node.id] then intuitiveLeaps[node.id] = self:NodesInIntuitiveLeapLikeRadius(n) else @@ -1517,6 +1593,29 @@ function PassiveSpecClass:BuildAllDependsAndPaths() end end end + for _, intuitiveLeapMap in ipairs(intuitiveLeapLikeNodes) do + local allocatable = false + for _, allocatableNodeType in ipairs(intuitiveLeapMap.to) do + if allocatableNodeType == depNode.type then + allocatable = true + break + end + end + if allocatable then + if intuitiveLeapMap.from == "Keystone" then + for keyName, keyNode in pairs(self.tree.keystoneMap) do + if keyNode.nodesInRadius and keyNode.nodesInRadius[intuitiveLeapMap.radiusIndex][depNode.id] then + -- Hold off on the pruning; this node could be supported by Intuitive Leap-like jewel + prune = false + if not intuitiveLeaps[keyNode.id] then + intuitiveLeaps[keyNode.id] = { } + end + t_insert(intuitiveLeaps[keyNode.id], depNode) + end + end + end + end + end if prune then self:DeallocSingleNode(depNode) end diff --git a/src/Classes/PassiveTreeView.lua b/src/Classes/PassiveTreeView.lua index 098665956..6a216691b 100644 --- a/src/Classes/PassiveTreeView.lua +++ b/src/Classes/PassiveTreeView.lua @@ -1055,6 +1055,23 @@ function PassiveTreeViewClass:Draw(build, viewPort, inputEvents) end end end + + -- Draw ring overlays for other sources of intuitive leap-like effects + for _, effectData in ipairs(spec.intuitiveLeapLikeNodes) do + local radData = build.data.jewelRadius[effectData.radiusIndex] + local outerSize = radData.outer * data.gameConstants["PassiveTreeJewelDistanceMultiplier"] * scale + if effectData.from == "Keystone" then + for keystoneName, keystoneNode in pairs(spec.tree.keystoneMap) do + if keystoneNode and spec.allocNodes[keystoneNode.id] + and keystoneName == keystoneNode.name -- keystoneMap contains both TitleCase and lowercase names, so we only need to draw once + and keystoneNode.x and keystoneNode.y then + local keyX, keyY = treeToScreen(keystoneNode.x, keystoneNode.y) + self:DrawImageRotated(self.jewelShadedOuterRing, keyX, keyY, outerSize * 2, outerSize * 2, -0.7) + self:DrawImageRotated(self.jewelShadedOuterRingFlipped, keyX, keyY, outerSize * 2, outerSize * 2, 0.7) + end + end + end + end end -- Draws the given asset at the given position diff --git a/src/Data/ModCache.lua b/src/Data/ModCache.lua index f6a4d2aee..92fd4df1d 100644 --- a/src/Data/ModCache.lua +++ b/src/Data/ModCache.lua @@ -5665,7 +5665,7 @@ c["Non-Channelling Spells deal 10% increased Damage per 100 maximum Life"]={{[1] c["Non-Channelling Spells deal 6% increased Damage per 100 maximum Life"]={{[1]={[1]={neg=true,skillType=48,type="SkillType"},[2]={div=100,stat="Life",type="PerStat"},flags=2,keywordFlags=0,name="Damage",type="INC",value=6}},nil} c["Non-Channelling Spells have 3% increased Critical Hit Chance per 100 maximum Life"]={{[1]={[1]={neg=true,skillType=48,type="SkillType"},[2]={div=100,stat="Life",type="PerStat"},flags=2,keywordFlags=0,name="CritChance",type="INC",value=3}},nil} c["Non-Channelling Spells have 5% increased Critical Hit Chance per 100 maximum Life"]={{[1]={[1]={neg=true,skillType=48,type="SkillType"},[2]={div=100,stat="Life",type="PerStat"},flags=2,keywordFlags=0,name="CritChance",type="INC",value=5}},nil} -c["Non-Keystone Passive Skills in Medium Radius of allocated Keystone Passive Skills can be allocated without being connected to your tree"]={nil,"Non-Keystone Passive Skills in Medium Radius of allocated Keystone Passive Skills can be allocated without being connected to your tree "} +c["Non-Keystone Passive Skills in Medium Radius of allocated Keystone Passive Skills can be allocated without being connected to your tree"]={{[1]={flags=0,keywordFlags=0,name="AllocateFromNodeRadius",type="LIST",value={from="Keystone",radiusIndex=2,to={[1]="Notable",[2]="Normal"}}}},nil} c["Non-Minion Skills have 50% less Reservation Efficiency"]={nil,"Non-Minion Skills have 50% less Reservation Efficiency "} c["Non-Unique Time-Lost Jewels have 40% increased radius"]={nil,"Non-Unique Time-Lost Jewels have 40% increased radius "} c["Offering Skills have 15% increased Buff effect"]={{[1]={[1]={skillType=154,type="SkillType"},flags=0,keywordFlags=0,name="BuffEffect",type="INC",value=15}},nil} diff --git a/src/Modules/ModParser.lua b/src/Modules/ModParser.lua index 527b3e9f4..9c5ca0e15 100644 --- a/src/Modules/ModParser.lua +++ b/src/Modules/ModParser.lua @@ -5317,6 +5317,9 @@ local specialModList = { } end, ["charms applied to you have (%d+)%% increased effect"] = function(num) return { mod("CharmEffect", "INC", num, { type = "ActorCondition", actor = "player"}) } end, -- Jewels + ["non%-keystone passive skills in medium radius of allocated keystone passive skills can be allocated without being connected to your tree"] = function(_, radius) return { + mod("AllocateFromNodeRadius", "LIST", { from = "Keystone", radiusIndex = 2, to = { "Notable", "Normal" } }), + } end, ["passives in radius of ([%a%s']+) can be allocated without being connected to your tree"] = function(_, name) return { mod("JewelData", "LIST", { key = "fromNothingKeystone", value = name }), mod("FromNothingKeystones", "LIST", { key = name, value = true }),