Skip to content

Commit c2ca27d

Browse files
committed
FEAT(mods): port generalized "doubled" mod handling from POB2
PathOfBuildingCommunity/PathOfBuilding-PoE2#1095
1 parent ea9dd66 commit c2ca27d

5 files changed

Lines changed: 72 additions & 67 deletions

File tree

docs/modSyntax.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ Often a mod will only apply under certain conditions, apply multiple times based
3636
* var: mod to multiply by
3737
* limit: The maximum number the mod can go up to
3838
* limitTotal: boolean that changes the behavior of limit to apply after multiplication. Defaults to false.
39+
* globalLimit: The maximum global number the mod can go up to, even with multiple sources. Useful for mods that say "up to a maximum of ..."
40+
* globalLimitKey: string identifier for the global limit. Mods with identical keys cannot go over the globalLimit.
3941
* MultiplierThreshold: Similar to a condition that only applies when the variable is above a specified threshold
4042
* var: name of the mod
4143
* threshold: number to reach before the mod applies

src/Classes/ModDB.lua

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,18 +123,28 @@ end
123123
function ModDBClass:MoreInternal(context, cfg, flags, keywordFlags, source, ...)
124124
local result = 1
125125
local modPrecision = nil
126+
local globalLimits = { }
126127
for i = 1, select('#', ...) do
127128
local modList = self.mods[select(i, ...)]
128129
local modResult = 1 --The more multipliers for each mod are computed to the nearest percent then applied.
129130
if modList then
130131
for i = 1, #modList do
131132
local mod = modList[i]
132133
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
134+
local value
133135
if mod[1] then
134-
modResult = modResult * (1 + (context:EvalMod(mod, cfg) or 0) / 100)
136+
value = context:EvalMod(mod, cfg) or 0
137+
if mod[1].globalLimit and mod[1].globalLimitKey then
138+
globalLimits[mod[1].globalLimitKey] = globalLimits[mod[1].globalLimitKey] or 0
139+
if globalLimits[mod[1].globalLimitKey] + value > mod[1].globalLimit then
140+
value = mod[1].globalLimit - globalLimits[mod[1].globalLimitKey]
141+
end
142+
globalLimits[mod[1].globalLimitKey] = globalLimits[mod[1].globalLimitKey] + value
143+
end
135144
else
136-
modResult = modResult * (1 + mod.value / 100)
145+
value = mod.value or 0
137146
end
147+
modResult = modResult * (1 + value / 100)
138148
if modPrecision then
139149
modPrecision = m_max(modPrecision, (data.highPrecisionMods[mod.name] and data.highPrecisionMods[mod.name][mod.type]) or modPrecision)
140150
else

src/Classes/ModStore.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ function ModStoreClass:GetCondition(var, cfg, noMod)
228228
end
229229

230230
function ModStoreClass:GetMultiplier(var, cfg, noMod)
231-
return (self.multipliers[var] or 0) + (self.parent and self.parent:GetMultiplier(var, cfg, true) or 0) + (not noMod and self:Sum("BASE", cfg, multiplierName[var]) or 0)
231+
return (not noMod and self:Override(cfg, multiplierName[var])) or (self.multipliers[var] or 0) + (self.parent and self.parent:GetMultiplier(var, cfg, true) or 0) + (not noMod and self:Sum("BASE", cfg, multiplierName[var]) or 0)
232232
end
233233

234234
function ModStoreClass:GetStat(stat, cfg)

src/Modules/CalcDefence.lua

Lines changed: 4 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -367,40 +367,18 @@ function calcs.defence(env, actor)
367367
if armourData then
368368
wardBase = not modDB:Flag(nil, "GainNoWardFrom" .. slot) and armourData.Ward or 0
369369
if wardBase > 0 then
370-
if slot == "Body Armour" and modDB:Flag(nil, "DoubleBodyArmourDefence") then
371-
wardBase = wardBase * 2
372-
end
373370
output["WardOn"..slot] = wardBase
374371
end
375372
energyShieldBase = not modDB:Flag(nil, "GainNoEnergyShieldFrom" .. slot) and armourData.EnergyShield or 0
376373
if energyShieldBase > 0 then
377-
if slot == "Body Armour" and modDB:Flag(nil, "DoubleBodyArmourDefence") then
378-
energyShieldBase = energyShieldBase * 2
379-
end
380374
output["EnergyShieldOn"..slot] = energyShieldBase
381375
end
382376
armourBase = not modDB:Flag(nil, "GainNoArmourFrom" .. slot) and armourData.Armour or 0
383377
if armourBase > 0 then
384-
if slot == "Body Armour" then
385-
if modDB:Flag(nil, "DoubleBodyArmourDefence") then
386-
armourBase = armourBase * 2
387-
end
388-
if modDB:Flag(nil, "Unbreakable") then
389-
armourBase = armourBase * 2
390-
end
391-
end
392378
output["ArmourOn"..slot] = armourBase
393379
end
394380
evasionBase = not modDB:Flag(nil, "GainNoEvasionFrom" .. slot) and armourData.Evasion or 0
395381
if evasionBase > 0 then
396-
if slot == "Body Armour" then
397-
if modDB:Flag(nil, "DoubleBodyArmourDefence") then
398-
evasionBase = evasionBase * 2
399-
end
400-
if modDB:Flag(nil, "Unbreakable") and modDB:Flag(nil, "IronReflexes") then
401-
evasionBase = evasionBase * 2
402-
end
403-
end
404382
output["EvasionOn"..slot] = evasionBase
405383
end
406384
end
@@ -699,9 +677,6 @@ function calcs.defence(env, actor)
699677
slotCfg.slotName = slot
700678
wardBase = not modDB:Flag(nil, "GainNoWardFrom" .. slot) and armourData.Ward or 0
701679
if wardBase > 0 then
702-
if slot == "Body Armour" and modDB:Flag(nil, "DoubleBodyArmourDefence") then
703-
wardBase = wardBase * 2
704-
end
705680
if modDB:Flag(nil, "EnergyShieldToWard") then
706681
local inc = modDB:Sum("INC", slotCfg, "Ward", "Defences", "EnergyShield")
707682
local more = modDB:More(slotCfg, "Ward", "Defences")
@@ -727,9 +702,6 @@ function calcs.defence(env, actor)
727702
end
728703
energyShieldBase = not modDB:Flag(nil, "GainNoEnergyShieldFrom" .. slot) and armourData.EnergyShield or 0
729704
if energyShieldBase > 0 then
730-
if slot == "Body Armour" and modDB:Flag(nil, "DoubleBodyArmourDefence") then
731-
energyShieldBase = energyShieldBase * 2
732-
end
733705
if modDB:Flag(nil, "EnergyShieldToWard") then
734706
local more = modDB:More(slotCfg, "EnergyShield", "Defences")
735707
energyShield = energyShield + energyShieldBase * more
@@ -753,16 +725,8 @@ function calcs.defence(env, actor)
753725
end
754726
armourBase = not modDB:Flag(nil, "GainNoArmourFrom" .. slot) and armourData.Armour or 0
755727
if armourBase > 0 then
756-
if slot == "Body Armour" then
757-
if modDB:Flag(nil, "DoubleBodyArmourDefence") then
758-
armourBase = armourBase * 2
759-
end
760-
if modDB:Flag(nil, "Unbreakable") then
761-
armourBase = armourBase * 2
762-
end
763-
if modDB:Flag(nil, "ConvertBodyArmourArmourEvasionToWard") then
764-
armourBase = armourBase * (1 - ((m_min(modDB:Sum("BASE", nil, "BodyArmourArmourEvasionToWardPercent"), 100) or 0) / 100))
765-
end
728+
if slot == "Body Armour" and modDB:Flag(nil, "ConvertBodyArmourArmourEvasionToWard")then
729+
armourBase = armourBase * (1 - ((m_min(modDB:Sum("BASE", nil, "BodyArmourArmourEvasionToWardPercent"), 100) or 0) / 100))
766730
end
767731
armour = armour + armourBase * calcLib.mod(modDB, slotCfg, "Armour", "ArmourAndEvasion", "Defences", slot.."ESAndArmour")
768732
gearArmour = gearArmour + armourBase
@@ -772,16 +736,8 @@ function calcs.defence(env, actor)
772736
end
773737
evasionBase = not modDB:Flag(nil, "GainNoEvasionFrom" .. slot) and armourData.Evasion or 0
774738
if evasionBase > 0 then
775-
if slot == "Body Armour" then
776-
if modDB:Flag(nil, "DoubleBodyArmourDefence") then
777-
evasionBase = evasionBase * 2
778-
end
779-
if modDB:Flag(nil, "Unbreakable") and ironReflexes then
780-
evasionBase = evasionBase * 2
781-
end
782-
if modDB:Flag(nil, "ConvertBodyArmourArmourEvasionToWard") then
783-
evasionBase = evasionBase * (1 - ((m_min(modDB:Sum("BASE", nil, "BodyArmourArmourEvasionToWardPercent"), 100) or 0) / 100))
784-
end
739+
if slot == "Body Armour" and modDB:Flag(nil, "ConvertBodyArmourArmourEvasionToWard")then
740+
evasionBase = evasionBase * (1 - ((m_min(modDB:Sum("BASE", nil, "BodyArmourArmourEvasionToWardPercent"), 100) or 0) / 100))
785741
end
786742
gearEvasion = gearEvasion + evasionBase
787743
if breakdown then

src/Modules/ModParser.lua

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@ local formList = {
142142
["^gain "] = "FLAG",
143143
["^you gain "] = "FLAG",
144144
["is (%-?%d+)%%? "] = "OVERRIDE",
145+
["is doubled"] = "DOUBLED",
146+
["doubles?"] = "DOUBLED",
147+
["causes? double"] = "DOUBLED",
145148
}
146149

147150
-- Map of modifier names
@@ -2032,11 +2035,6 @@ local specialModList = {
20322035
},
20332036
["life regeneration is applied to energy shield instead"] = { flag("ZealotsOath") },
20342037
["life leeched per second is doubled"] = { mod("LifeLeechRate", "MORE", 100) },
2035-
["total recovery per second from life leech is doubled"] = { mod("LifeLeechRate", "MORE", 100) },
2036-
["maximum total recovery per second from life leech is doubled"] = { mod("MaxLifeLeechRate", "MORE", 100) },
2037-
["maximum total life recovery per second from leech is doubled"] = { mod("MaxLifeLeechRate", "MORE", 100) },
2038-
["maximum total recovery per second from energy shield leech is doubled"] = { mod("MaxEnergyShieldLeechRate", "MORE", 100) },
2039-
["maximum total energy shield recovery per second from leech is doubled"] = { mod("MaxEnergyShieldLeechRate", "MORE", 100) },
20402038
["life regeneration has no effect"] = { flag("NoLifeRegen") },
20412039
["energy shield recharge instead applies to life"] = { flag("EnergyShieldRechargeAppliesToLife") },
20422040
["deal no non%-fire damage"] = { flag("DealNoPhysical"), flag("DealNoLightning"), flag("DealNoCold"), flag("DealNoChaos") },
@@ -2385,7 +2383,6 @@ local specialModList = {
23852383
-- Deadeye
23862384
["projectiles pierce all nearby targets"] = { flag("PierceAllTargets") },
23872385
["gain %+(%d+) life when you hit a bleeding enemy"] = function(num) return { mod("LifeOnHit", "BASE", num, nil, ModFlag.Hit, { type = "ActorCondition", actor = "enemy", var = "Bleeding" }) } end,
2388-
["accuracy rating is doubled"] = { mod("Accuracy", "MORE", 100) },
23892386
["(%d+)%% increased blink arrow and mirror arrow cooldown recovery speed"] = function(num) return {
23902387
mod("CooldownRecovery", "INC", num, { type = "SkillName", skillNameList = { "Blink Arrow", "Mirror Arrow" }, includeTransfigured = true }),
23912388
} end,
@@ -2629,8 +2626,6 @@ local specialModList = {
26292626
mod("Damage", "MORE", num, nil, ModFlag.Attack, { type = "Multiplier", var = "CastLast8Seconds", limit = max, limitTotal = true }),
26302627
} end,
26312628
-- Juggernaut
2632-
["armour received from body armour is doubled"] = { flag("Unbreakable") },
2633-
["armour from equipped body armour is doubled"] = { flag("Unbreakable") },
26342629
["action speed cannot be modified to below base value"] = { mod("MinimumActionSpeed", "MAX", 100, { type = "GlobalEffect", effectType = "Global", unscalable = true }) },
26352630
["movement speed cannot be modified to below base value"] = { flag("MovementSpeedCannotBeBelowBase") },
26362631
["you cannot be slowed to below base speed"] = { mod("MinimumActionSpeed", "MAX", 100, { type = "GlobalEffect", effectType = "Global", unscalable = true }) },
@@ -2752,7 +2747,10 @@ local specialModList = {
27522747
},
27532748
["(%d+)%% more elemental damage while unbound"] = function(num) return { mod("ElementalDamage", "MORE", num, { type = "Condition", var = "Unbound"})} end,
27542749
-- Warden (Affliction)
2755-
["defences from equipped body armour are doubled if it has no socketed gems"] = { flag("DoubleBodyArmourDefence", { type = "MultiplierThreshold", var = "SocketedGemsInBody Armour", threshold = 0, upper = true }, { type = "Condition", var = "UsingBody Armour" }) },
2750+
["defences from equipped body armour are doubled if it has no socketed gems"] = {
2751+
mod("Defences", "MORE", 100, { type = "MultiplierThreshold", var = "SocketedGemsInBody Armour", threshold = 0, upper = true }, { type = "Condition", var = "UsingBody Armour" }, { type = "SlotName", slotName = "Body Armour"}, { type = "Multiplier", var = "OathoftheMajiDoubled", globalLimit = 100, globalLimitKey = "OathoftheMajiLimit" }),
2752+
mod("Multiplier:OathoftheMajiDoubled", "OVERRIDE", 1, { type = "SlotName", slotName = "Body Armour"}),
2753+
},
27562754
["([%+%-]%d+)%% to all elemental resistances if you have an equipped helmet with no socketed gems"] = function(num) return { mod("ElementalResist", "BASE", num, { type = "MultiplierThreshold", var = "SocketedGemsInHelmet", threshold = 0, upper = true}, { type = "Condition", var = "UsingHelmet" }) } end,
27572755
["(%d+)%% increased maximum life if you have equipped gloves with no socketed gems"] = function(num) return { mod("Life", "INC", num, { type = "MultiplierThreshold", var = "SocketedGemsInGloves", threshold = 0, upper = true}, { type = "Condition", var = "UsingGloves" }) } end,
27582756
["(%d+)%% increased movement speed if you have equipped boots with no socketed gems"] = function(num) return { mod("MovementSpeed", "INC", num, { type = "MultiplierThreshold", var = "SocketedGemsInBoots", threshold = 0, upper = true}, { type = "Condition", var = "UsingBoots" }) } end,
@@ -4271,7 +4269,6 @@ local specialModList = {
42714269
["cold resistance is (%d+)%%"] = function(num) return { mod("ColdResist", "OVERRIDE", num) } end,
42724270
["lightning resistance is (%d+)%%"] = function(num) return { mod("LightningResist", "OVERRIDE", num) } end,
42734271
["elemental resistances are capped by your highest maximum elemental resistance instead"] = { flag("ElementalResistMaxIsHighestResistMax") },
4274-
["chaos resistance is doubled"] = { mod("ChaosResist", "MORE", 100) },
42754272
["nearby enemies have (%d+)%% increased fire and cold resistances"] = function(num) return {
42764273
mod("EnemyModifier", "LIST", { mod = mod("FireResist", "INC", num) }),
42774274
mod("EnemyModifier", "LIST", { mod = mod("ColdResist", "INC", num) }),
@@ -5954,6 +5951,10 @@ local function parseMod(line, order)
59545951
modFlag = modFlag
59555952
modExtraTags = { tag = { type = "Condition", var = "{Hand}Attack" } }
59565953
modSuffix, line = scan(line, suffixTypes, true)
5954+
elseif modForm == "GRANTS_GLOBAL" then
5955+
modType = "BASE"
5956+
modFlag = modFlag
5957+
modSuffix, line = scan(line, suffixTypes, true)
59575958
elseif modForm == "REMOVES" then -- local
59585959
modValue = -modValue
59595960
modType = "BASE"
@@ -6017,6 +6018,22 @@ local function parseMod(line, order)
60176018
modValue = type(modValue) == "table" and modValue.value or true
60186019
elseif modForm == "OVERRIDE" then
60196020
modType = "OVERRIDE"
6021+
elseif modForm == "DOUBLED" then
6022+
local modNameString
6023+
-- Need to assign two mod names. One actual "MORE" mod and one multiplier with a limit to prevent applying more than once
6024+
if type(modName) == "table" then
6025+
modNameString = modName[1]
6026+
modName[2] = "Multiplier:" .. modNameString .. "Doubled"
6027+
else
6028+
modNameString = modName
6029+
modName = modName and {modName, "Multiplier:" .. modName .. "Doubled"}
6030+
end
6031+
if modName then
6032+
modType = { "MORE", "OVERRIDE" }
6033+
modValue = { 100, 1 }
6034+
modExtraTags = { tag = true }
6035+
modExtraTags[1] = { tag = { type = "Multiplier", var = modNameString .. "Doubled", globalLimit = 100, globalLimitKey = modNameString .. "DoubledLimit" }}
6036+
end
60206037
end
60216038
if not modName then
60226039
return { }, line
@@ -6026,16 +6043,35 @@ local function parseMod(line, order)
60266043
local flags = 0
60276044
local keywordFlags = 0
60286045
local tagList = { }
6046+
local modTagList -- need this in case of multiple mods with separate tags
60296047
local misc = { }
60306048
for _, data in pairs({ modName, preFlag, modFlag, modTag, modTag2, skillTag, modExtraTags }) do
60316049
if type(data) == "table" then
60326050
flags = bor(flags, data.flags or 0)
60336051
keywordFlags = bor(keywordFlags, data.keywordFlags or 0)
60346052
if data.tag then
6035-
t_insert(tagList, copyTable(data.tag))
6053+
if data[1] and data[1].tag then -- Special handling for multiple mods with different tags within the same modExtraTags
6054+
modTagList = {}
6055+
for i, entry in ipairs(data) do
6056+
modTagList[i] = {}
6057+
if entry.tag then t_insert(modTagList[i], copyTable(entry.tag)) end
6058+
end
6059+
else
6060+
t_insert(tagList, copyTable(data.tag))
6061+
end
60366062
elseif data.tagList then
6037-
for _, tag in ipairs(data.tagList) do
6038-
t_insert(tagList, copyTable(tag))
6063+
if data[1] and data[1].tagList then -- Special handling for multiple mods with different tags within the same modExtraTags
6064+
modTagList = {}
6065+
for i, entry in ipairs(data) do
6066+
modTagList[i] = {}
6067+
for _, tag in ipairs(entry.tagList) do
6068+
t_insert(modTagList[i], copyTable(tag))
6069+
end
6070+
end
6071+
else
6072+
for _, tag in ipairs(data.tagList) do
6073+
t_insert(tagList, copyTable(tag))
6074+
end
60396075
end
60406076
end
60416077
for k, v in pairs(data) do
@@ -6050,16 +6086,17 @@ local function parseMod(line, order)
60506086
for i, name in ipairs(type(nameList) == "table" and nameList or { nameList }) do
60516087
modList[i] = {
60526088
name = name .. (modSuffix or misc.modSuffix or ""),
6053-
type = modType,
6089+
type = type(modType) == "table" and modType[i] or modType,
60546090
value = type(modValue) == "table" and modValue[i] or modValue,
60556091
flags = flags,
60566092
keywordFlags = keywordFlags,
6057-
unpack(tagList)
6093+
unpack(tagList),
60586094
}
6095+
if modTagList and modTagList[i] then t_insert(modList[i], unpack(modTagList[i])) end
60596096
end
60606097
if modList[1] then
60616098
-- Special handling for various modifier types
6062-
if misc.addToAura then
6099+
if misc.addToAura then
60636100
if misc.onlyAddToBanners then
60646101
for i, effectMod in ipairs(modList) do
60656102
modList[i] = mod("ExtraAuraEffect", "LIST", { mod = effectMod }, { type = "SkillType", skillType = SkillType.Banner })

0 commit comments

Comments
 (0)