Skip to content

Commit ddec29a

Browse files
Wires77LocalIdentity
andauthored
Add Ruthless tree (#6367)
* Add support for Ruthless trees * Fix export URL for Ruthless trees * Removed data.json * Fix PR comments * Fix edge cases around converting and importing * Updating support for 3rd party ruthless tree links * Update tree and add Tattoos * Add support for new Ruthless tree mods * Fix comment --------- Co-authored-by: LocalIdentity <localidentity2@gmail.com>
1 parent 0d84d1c commit ddec29a

22 files changed

+136262
-22
lines changed

src/Classes/ImportTab.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -602,7 +602,7 @@ function ImportTabClass:ImportPassiveTreeAndJewels(json, charData)
602602
end
603603
end
604604
end
605-
self.build.spec:ImportFromNodeList(charData.classId, charData.ascendancyClass, charPassiveData.hashes, charPassiveData.mastery_effects or {}, latestTreeVersion)
605+
self.build.spec:ImportFromNodeList(charData.classId, charData.ascendancyClass, charPassiveData.hashes, charPassiveData.mastery_effects or {}, latestTreeVersion .. (charData.league:match("Ruthless") and "_ruthless" or ""))
606606
self.build.spec:AddUndoState()
607607
self.build.characterLevel = charData.level
608608
self.build.characterLevelAutoMode = false

src/Classes/ModStore.lua

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,13 +390,19 @@ function ModStoreClass:EvalMod(mod, cfg)
390390
end
391391
elseif tag.type == "PercentStat" then
392392
local base
393+
local target = self
394+
-- This functions similar to the above tagTypes in regard to which actor to use, but for PerStat
395+
-- if the actor is 'parent', we don't want to return if we're already using 'parent', just keep using 'self'
396+
if tag.actor and self.actor[tag.actor] then
397+
target = self.actor[tag.actor].modDB
398+
end
393399
if tag.statList then
394400
base = 0
395401
for _, stat in ipairs(tag.statList) do
396-
base = base + self:GetStat(stat, cfg)
402+
base = base + target:GetStat(stat, cfg)
397403
end
398404
else
399-
base = self:GetStat(tag.stat, cfg)
405+
base = target:GetStat(tag.stat, cfg)
400406
end
401407
local mult = base * (tag.percent and tag.percent / 100 or 1)
402408
local limitTotal

src/Classes/TreeTab.lua

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -132,15 +132,12 @@ local TreeTabClass = newClass("TreeTab", "ControlHost", function(self, build)
132132
end
133133
self.treeVersions = { }
134134
for _, num in ipairs(treeVersionList) do
135-
if not num:find("^2") then
136-
local vers = num:gsub("%_", ".")
137-
t_insert(self.treeVersions, vers)
138-
end
135+
t_insert(self.treeVersions, treeVersions[num].display)
139136
end
140137
self.controls.versionText = new("LabelControl", { "LEFT", self.controls.export, "RIGHT" }, 8, 0, 0, 16, "Version:")
141-
self.controls.versionSelect = new("DropDownControl", { "LEFT", self.controls.versionText, "RIGHT" }, 8, 0, 55, 20, self.treeVersions, function(index, value)
138+
self.controls.versionSelect = new("DropDownControl", { "LEFT", self.controls.versionText, "RIGHT" }, 8, 0, 100, 20, self.treeVersions, function(index, value)
142139
if value ~= self.build.spec.treeVersion then
143-
convertToVersion(value:gsub("%.", "_"))
140+
convertToVersion(value:gsub("[%(%)]", ""):gsub("[%.%s]", "_"))
144141
end
145142
end)
146143
self.controls.versionSelect.maxDroppedWidth = 1000
@@ -199,8 +196,14 @@ local TreeTabClass = newClass("TreeTab", "ControlHost", function(self, build)
199196
self.controls.specConvertText.shown = function()
200197
return self.showConvert
201198
end
202-
self.controls.specConvert = new("ButtonControl", { "LEFT", self.controls.specConvertText, "RIGHT" }, 8, 0, 120, 20, "^2Convert to "..treeVersions[latestTreeVersion].display, function()
203-
convertToVersion(latestTreeVersion)
199+
local function getLatestTreeVersion()
200+
return latestTreeVersion .. (self.specList[self.activeSpec].treeVersion:match("^" .. latestTreeVersion .. "(.*)") or "")
201+
end
202+
local function buildConvertButtonLabel()
203+
return "^2Convert to "..treeVersions[getLatestTreeVersion()].display
204+
end
205+
self.controls.specConvert = new("ButtonControl", { "LEFT", self.controls.specConvertText, "RIGHT" }, 8, 0, function() return DrawStringWidth(16, "VAR", buildConvertButtonLabel()) + 20 end, 20, buildConvertButtonLabel, function()
206+
convertToVersion(getLatestTreeVersion())
204207
end)
205208
self.jumpToNode = false
206209
self.jumpToX = 0
@@ -232,15 +235,15 @@ function TreeTabClass:Draw(viewPort, inputEvents)
232235

233236
-- Determine positions if one line of controls doesn't fit in the screen width
234237
local twoLineHeight = 24
235-
if viewPort.width >= 1168 + (self.isComparing and 198 or 0) + (self.viewer.showHeatMap and 316 or 0) then
238+
if viewPort.width >= 1336 + (self.isComparing and 198 or 0) + (self.viewer.showHeatMap and 316 or 0) then
236239
twoLineHeight = 0
237-
self.controls.findTimelessJewel:SetAnchor("LEFT", self.controls.treeSearch, "RIGHT", 8, 0)
240+
self.controls.treeSearch:SetAnchor("LEFT", self.controls.versionSelect, "RIGHT", 8, 0)
238241
if self.controls.powerReportList then
239242
self.controls.powerReportList:SetAnchor("TOPLEFT", self.controls.specSelect, "BOTTOMLEFT", 0, self.controls.specSelect.height + 4)
240243
self.controls.allocatedNodeToggle:SetAnchor("TOPLEFT", self.controls.powerReportList, "TOPRIGHT", 8, 0)
241244
end
242245
else
243-
self.controls.findTimelessJewel:SetAnchor("TOPLEFT", self.controls.specSelect, "BOTTOMLEFT", 0, 4)
246+
self.controls.treeSearch:SetAnchor("TOPLEFT", self.controls.specSelect, "BOTTOMLEFT", 0, 4)
244247
if self.controls.powerReportList then
245248
self.controls.powerReportList:SetAnchor("TOPLEFT", self.controls.findTimelessJewel, "BOTTOMLEFT", 0, self.controls.treeHeatMap.y + self.controls.treeHeatMap.height + 4)
246249
self.controls.allocatedNodeToggle:SetAnchor("TOPLEFT", self.controls.powerReportList, "TOPRIGHT", -76, -44)
@@ -382,7 +385,7 @@ function TreeTabClass:SetActiveSpec(specId)
382385
end
383386
end
384387
end
385-
self.showConvert = curSpec.treeVersion ~= latestTreeVersion
388+
self.showConvert = not curSpec.treeVersion:match("^" .. latestTreeVersion)
386389
if self.build.itemsTab.itemOrderList[1] then
387390
-- Update item slots if items have been loaded already
388391
self.build.itemsTab:PopulateSlots()
@@ -434,20 +437,20 @@ function TreeTabClass:OpenImportPopup()
434437
main:ClosePopup()
435438
end
436439
end
437-
local function validateTreeVersion(major, minor)
440+
local function validateTreeVersion(isRuthless, major, minor)
438441
-- Take the Major and Minor version numbers and confirm it is a valid tree version. The point release is also passed in but it is not used
439442
-- Return: the passed in tree version as text or latestTreeVersion
440443
if major and minor then
441444
--need leading 0 here
442445
local newTreeVersionNum = tonumber(string.format("%d.%02d", major, minor))
443446
if newTreeVersionNum >= treeVersions[defaultTreeVersion].num and newTreeVersionNum <= treeVersions[latestTreeVersion].num then
444447
-- no leading 0 here
445-
return string.format("%s_%s", major, minor)
448+
return string.format("%s_%s", major, minor) .. isRuthless and "_ruthless" or ""
446449
else
447450
print(string.format("Version '%d_%02d' is out of bounds", major, minor))
448451
end
449452
end
450-
return latestTreeVersion
453+
return latestTreeVersion .. (isRuthless and "_ruthless" or "")
451454
end
452455

453456
controls.editLabel = new("LabelControl", nil, 0, 20, 0, 16, "Enter passive tree link:")
@@ -487,18 +490,19 @@ function TreeTabClass:OpenImportPopup()
487490
controls.import.enabled = true
488491
return
489492
else
490-
decodeTreeLink(treeLink, validateTreeVersion(treeLink:match(versionLookup)))
493+
decodeTreeLink(treeLink, validateTreeVersion(treeLink:match("tree/ruthless"), treeLink:match(versionLookup)))
491494
end
492495
end)
493496
end
494497
elseif treeLink:match("poeskilltree.com/") then
495498
local oldStyleVersionLookup = "/%?v=([0-9]+)%.([0-9]+)%.([0-9]+)#"
496499
-- Strip the version from the tree : https://poeskilltree.com/?v=3.6.0#AAAABAMAABEtfIOFMo6-ksHfsOvu -> https://poeskilltree.com/AAAABAMAABEtfIOFMo6-ksHfsOvu
497-
decodeTreeLink(treeLink:gsub("/%?v=.+#","/"), validateTreeVersion(treeLink:match(oldStyleVersionLookup)))
500+
decodeTreeLink(treeLink:gsub("/%?v=.+#","/"), validateTreeVersion(treeLink:match("tree/ruthless"), treeLink:match(oldStyleVersionLookup)))
498501
else
499502
-- EG: https://www.pathofexile.com/passive-skill-tree/3.15.0/AAAABgMADI6-HwKSwQQHLJwtH9-wTLNfKoP3ES3r5AAA
500503
-- EG: https://www.pathofexile.com/fullscreen-passive-skill-tree/3.15.0/AAAABgMADAQHES0fAiycLR9Ms18qg_eOvpLB37Dr5AAA
501-
decodeTreeLink(treeLink, validateTreeVersion(treeLink:match(versionLookup)))
504+
-- EG: https://www.pathofexile.com/passive-skill-tree/ruthless/AAAABgAAAAAA (Ruthless doesn't have versions)
505+
decodeTreeLink(treeLink, validateTreeVersion(treeLink:match("tree/ruthless"), treeLink:match(versionLookup)))
502506
end
503507
end)
504508
controls.cancel = new("ButtonControl", nil, 45, 80, 80, 20, "Cancel", function()

src/GameVersions.lua

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ liveTargetVersion = "3_0"
77
-- Skill tree versions
88
---Added for convenient indexing of skill tree versions.
99
---@type string[]
10-
treeVersionList = { "2_6", "3_6", "3_7", "3_8", "3_9", "3_10", "3_11", "3_12", "3_13", "3_14", "3_15", "3_16", "3_17", "3_18", "3_19", "3_20", "3_21", "3_22", }
10+
treeVersionList = { "2_6", "3_6", "3_7", "3_8", "3_9", "3_10", "3_11", "3_12", "3_13", "3_14", "3_15", "3_16", "3_17", "3_18", "3_19", "3_20", "3_21", "3_22_ruthless", "3_22", }
1111
--- Always points to the latest skill tree version.
1212
latestTreeVersion = treeVersionList[#treeVersionList]
1313
---Tree version where multiple skill trees per build were introduced to PoBC.
@@ -100,6 +100,11 @@ treeVersions = {
100100
num = 3.21,
101101
url = "https://www.pathofexile.com/passive-skill-tree/3.21.0/",
102102
},
103+
["3_22_ruthless"] = {
104+
display = "3.22 (ruthless)",
105+
num = 3.22,
106+
url = "https://www.pathofexile.com/passive-skill-tree/ruthless/",
107+
},
103108
["3_22"] = {
104109
display = "3.22",
105110
num = 3.22,

src/Modules/ModParser.lua

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1875,6 +1875,9 @@ local specialModList = {
18751875
["totems explode on death, dealing (%d+)%% of their life as (.+) damage"] = function(amount, _, type) -- Crucible weapon mod
18761876
return explodeFunc(100, amount, type)
18771877
end,
1878+
["nearby corpses explode when you warcry, dealing (%d+)%% of their life as (.+) damage"] = function(amount, _, type) -- Ruthless Berserker node
1879+
return explodeFunc(100, amount, type)
1880+
end,
18781881
-- Keystones
18791882
["(%d+) rage regenerated for every (%d+) mana regeneration per second"] = function(num, _, div) return {
18801883
mod("RageRegen", "BASE", num, {type = "PerStat", stat = "ManaRegen", div = tonumber(div) }) ,
@@ -2160,6 +2163,7 @@ local specialModList = {
21602163
flag("Condition:CanGainRage", { type = "ActorCondition", actor = "enemy", var = "PinnacleBoss" }),
21612164
},
21622165
["inherent effects from having rage are tripled"] = { mod("Multiplier:RageEffect", "BASE", 2) },
2166+
["inherent effects from having rage are doubled"] = { mod("Multiplier:RageEffect", "BASE", 1) },
21632167
["cannot be stunned while you have at least (%d+) rage"] = function(num) return { flag("StunImmune", { type = "MultiplierThreshold", var = "Rage", threshold = num }) } end,
21642168
["lose ([%d%.]+)%% of life per second per rage while you are not losing rage"] = function(num) return { mod("LifeDegen", "BASE", 1, { type = "PercentStat", stat = "Life", percent = num }, { type = "Multiplier", var = "Rage" }) } end,
21652169
["if you've warcried recently, you and nearby allies have (%d+)%% increased attack speed"] = function(num) return { mod("ExtraAura", "LIST", { mod = mod("Speed", "INC", num, nil, ModFlag.Attack) }, { type = "Condition", var = "UsedWarcryRecently" }) } end,
@@ -2375,6 +2379,9 @@ local specialModList = {
23752379
["attack damage is lucky if you[' ]h?a?ve blocked in the past (%d+) seconds"] = {
23762380
flag("LuckyHits", nil, ModFlag.Attack, { type = "Condition", var = "BlockedRecently" })
23772381
},
2382+
["attack damage while dual wielding is lucky if you[' ]h?a?ve blocked in the past (%d+) seconds"] = {
2383+
flag("LuckyHits", nil, ModFlag.Attack, { type = "Condition", var = "BlockedRecently" }, { type = "Condition", var = "DualWielding" })
2384+
},
23782385
["hits ignore enemy monster physical damage reduction if you[' ]h?a?ve blocked in the past (%d+) seconds"] = {
23792386
flag("IgnoreEnemyPhysicalDamageReduction", { type = "Condition", var = "BlockedRecently" })
23802387
},
@@ -2384,6 +2391,7 @@ local specialModList = {
23842391
} end,
23852392
-- Guardian
23862393
["grants armour equal to (%d+)%% of your reserved life to you and nearby allies"] = function(num) return { mod("GrantReservedLifeAsAura", "LIST", { mod = mod("Armour", "BASE", num / 100) }) } end,
2394+
["grants armour equal to (%d+)%% of your reserved mana to you and nearby allies"] = function(num) return { mod("GrantReservedManaAsAura", "LIST", { mod = mod("Armour", "BASE", num / 100) }) } end,
23872395
["grants maximum energy shield equal to (%d+)%% of your reserved mana to you and nearby allies"] = function(num) return { mod("GrantReservedManaAsAura", "LIST", { mod = mod("EnergyShield", "BASE", num / 100) }) } end,
23882396
["grants armour equal to (%d+)%% of your reserved mana to you and nearby allies"] = function(num) return { mod("GrantReservedManaAsAura", "LIST", { mod = mod("Armour", "BASE", num / 100) }) } end,
23892397
["warcries cost no mana"] = { mod("ManaCost", "MORE", -100, nil, 0, KeywordFlag.Warcry) },
@@ -2477,6 +2485,11 @@ local specialModList = {
24772485
["regenerate (%d+)%% of mana over 2 seconds when you consume a corpse"] = function(num) return { mod("ManaRegen", "BASE", 1, { type = "PercentStat", stat = "Mana", percent = num / 2 }, { type = "Condition", var = "ConsumedCorpseInPast2Sec" }) } end,
24782486
["corpses you spawn have (%d+)%% increased maximum life"] = function(num) return { mod("CorpseLife", "INC", num) } end,
24792487
["corpses you spawn have (%d+)%% reduced maximum life"] = function(num) return { mod("CorpseLife", "INC", -num) } end,
2488+
["minions gain added physical damage equal to (%d+)%% of maximum energy shield on your equipped helmet"] = function(num) return {
2489+
mod("MinionModifier", "LIST", { mod = mod("PhysicalMin", "BASE", 1, { type = "PercentStat", stat = "EnergyShieldOnHelmet", actor = "parent", percent = num }) }),
2490+
mod("MinionModifier", "LIST", { mod = mod("PhysicalMax", "BASE", 1, { type = "PercentStat", stat = "EnergyShieldOnHelmet", actor = "parent", percent = num }) }),
2491+
2492+
} end,
24802493
-- Occultist
24812494
["when you kill an enemy, for each curse on that enemy, gain (%d+)%% of non%-chaos damage as extra chaos damage for 4 seconds"] = function(num) return {
24822495
mod("NonChaosDamageGainAsChaos", "BASE", num, { type = "Condition", var = "KilledRecently" }, { type = "Multiplier", var = "CurseOnEnemy" }),
@@ -2519,6 +2532,11 @@ local specialModList = {
25192532
mod("EnemyModifier", "LIST", { mod = mod("ColdExposure", "BASE", -num) }, { type = "Condition", var = "Phasing" }),
25202533
mod("EnemyModifier", "LIST", { mod = mod("LightningExposure", "BASE", -num) }, { type = "Condition", var = "Phasing" }),
25212534
} end,
2535+
["nearby enemies have fire, cold and lightning exposure while you have phasing"] = {
2536+
mod("EnemyModifier", "LIST", { mod = mod("FireExposure", "BASE", -10) }, { type = "Condition", var = "Phasing" }),
2537+
mod("EnemyModifier", "LIST", { mod = mod("ColdExposure", "BASE", -10) }, { type = "Condition", var = "Phasing" }),
2538+
mod("EnemyModifier", "LIST", { mod = mod("LightningExposure", "BASE", -10) }, { type = "Condition", var = "Phasing" }),
2539+
},
25222540
-- Saboteur
25232541
["hits have (%d+)%% chance to deal (%d+)%% more area damage"] = function (num, _, more) return {
25242542
mod("Damage", "MORE", (num*more/100), nil, bor(ModFlag.Area, ModFlag.Hit))
@@ -2540,6 +2558,7 @@ local specialModList = {
25402558
["gain (%d+)%% increased movement speed for 20 seconds when you kill an enemy"] = function(num) return { mod("MovementSpeed", "INC", num, { type = "Condition", var = "KilledRecently" }) } end,
25412559
["gain (%d+)%% increased attack speed for 20 seconds when you kill a rare or unique enemy"] = function(num) return { mod("Speed", "INC", num, nil, ModFlag.Attack, 0, { type = "Condition", var = "KilledUniqueEnemy" }) } end,
25422560
["kill enemies that have (%d+)%% or lower life when hit by your skills"] = function(num) return { mod("CullPercent", "MAX", num) } end,
2561+
["you are unaffected by bleeding while leeching"] = { mod("SelfBleedEffect", "MORE", -100, { type = "Condition", var = "Leeching" }) },
25432562
-- Trickster
25442563
["(%d+)%% chance to gain (%d+)%% of non%-chaos damage with hits as extra chaos damage"] = function(num, _, perc) return { mod("NonChaosDamageGainAsChaos", "BASE", num / 100 * tonumber(perc)) } end,
25452564
["movement skills cost no mana"] = { mod("ManaCost", "MORE", -100, nil, 0, KeywordFlag.Movement) },
@@ -3391,6 +3410,9 @@ local specialModList = {
33913410
mod("ExtraAura", "LIST", { mod = flag("Condition:ArcaneSurge")}, { type = "Condition", var = "UsedWarcryRecently" }),
33923411
mod("ArcaneSurgeEffect", "INC", num, { type = "PerStat", stat = "WarcryPower", div = tonumber(div), globalLimit = tonumber(limit), globalLimitKey = "Brinerot Flag"}, { type = "Condition", var = "UsedWarcryRecently" }),
33933412
} end,
3413+
["gain arcane surge after spending a total of (%d+) mana"] = function(num) return {
3414+
mod("ExtraAura", "LIST", { mod = flag("Condition:ArcaneSurge")}, { type = "MultiplierThreshold", var = "ManaSpentRecently", threshold = num }),
3415+
} end,
33943416
["gain onslaught for (%d+) seconds on hit while at maximum frenzy charges"] = { flag("Onslaught", { type = "StatThreshold", stat = "FrenzyCharges", thresholdStat = "FrenzyChargesMax" }, { type = "Condition", var = "HitRecently" }) },
33953417
["enemies in your chilling areas take (%d+)%% increased lightning damage"] = function(num) return { mod("EnemyModifier", "LIST", { mod = mod("LightningDamageTaken", "INC", num) }, { type = "ActorCondition", actor = "enemy", var = "InChillingArea" }) } end,
33963418
["warcries count as having (%d+) additional nearby enemies"] = function(num) return {
50.3 KB
Loading
666 KB
Loading
7.12 KB
Loading
154 KB
Loading
743 KB
Loading

0 commit comments

Comments
 (0)