From fa32157d28b3872ad6ac259ea78c064714b2040e Mon Sep 17 00:00:00 2001 From: SaloEater Date: Wed, 22 Jul 2026 03:34:56 -0500 Subject: [PATCH 1/9] feature: Roiling Tempest notable --- spec/System/TestOffence_spec.lua | 137 +++++++++++++++++++++++++++++++ src/Modules/CalcOffence.lua | 6 +- src/Modules/ModParser.lua | 83 +++++++++++-------- 3 files changed, 190 insertions(+), 36 deletions(-) create mode 100644 spec/System/TestOffence_spec.lua diff --git a/spec/System/TestOffence_spec.lua b/spec/System/TestOffence_spec.lua new file mode 100644 index 0000000000..c96b4b2d41 --- /dev/null +++ b/spec/System/TestOffence_spec.lua @@ -0,0 +1,137 @@ +describe("TestMinMaxDamageMods", function() + before_each(function() + newBuild() + end) + + teardown(function() + -- newBuild() takes care of resetting everything in setup() + end) + + it("parses more/less/increased/reduced minimum and maximum damage of every type", function() + build.itemsTab:CreateDisplayItemFromRaw([[ + New Item + Coral Amulet + 25% more Maximum Lightning Damage + 50% less Minimum Lightning Damage + 10% increased Maximum Cold Damage + 12% reduced Minimum Physical Damage + 20% more Maximum Chaos Damage + 30% more Maximum Fire Damage + ]]) + build.itemsTab:AddDisplayItem() + runCallback("OnFrame") + + local modDB = build.calcsTab.mainEnv.player.modDB + + -- "more"/"less" -> MORE modifier (More() returns the aggregate multiplier) + assert.are.equals(1.25, modDB:More(nil, "MaxLightningDamage")) + assert.are.equals(0.5, modDB:More(nil, "MinLightningDamage")) + assert.are.equals(1.2, modDB:More(nil, "MaxChaosDamage")) + assert.are.equals(1.3, modDB:More(nil, "MaxFireDamage")) + + -- "increased"/"reduced" -> INC modifier + assert.are.equals(10, modDB:Sum("INC", nil, "MaxColdDamage")) + assert.are.equals(-12, modDB:Sum("INC", nil, "MinPhysicalDamage")) + + -- sanity: these must not have leaked into the wrong stat/type + assert.are.equals(1, modDB:More(nil, "MinChaosDamage")) + assert.are.equals(0, modDB:Sum("INC", nil, "MaxLightningDamage")) + end) + + -- calcDamage rounds each min/max to an integer before our multipliers' effects can be + -- observed, so scaling the rounded baseline can differ from the real value by ~1 + local function assertNear(expected, actual, msg) + assert.is_true(math.abs(expected - actual) <= 2, string.format("%s: expected ~%.2f, got %.2f", msg, expected, actual)) + end + + it("applies min/max damage mods to an actual skill in CalcOffence", function() + build.skillsTab:PasteSocketGroup("Slot: Body Armour\nArc 20/0 1\n") + runCallback("OnFrame") + + local baseMin = build.calcsTab.calcsOutput.LightningMin + local baseMax = build.calcsTab.calcsOutput.LightningMax + assert.is_true(baseMax > 0) + assert.is_true(baseMin > 0) + + -- MORE path: "more"/"less" scale only the targeted end of the roll + build.itemsTab:CreateDisplayItemFromRaw([[ + New Item + Coral Amulet + 25% more Maximum Lightning Damage + 50% less Minimum Lightning Damage + ]]) + build.itemsTab:AddDisplayItem() + runCallback("OnFrame") + + assertNear(baseMax * 1.25, build.calcsTab.calcsOutput.LightningMax, "25%% more max") + assertNear(baseMin * 0.5, build.calcsTab.calcsOutput.LightningMin, "50%% less min") + + -- INC path: "increased" stacks multiplicatively with the MORE factors above + build.itemsTab:CreateDisplayItemFromRaw([[ + New Item + Coral Ring + 40% increased Maximum Lightning Damage + 100% increased Minimum Lightning Damage + ]]) + build.itemsTab:AddDisplayItem() + runCallback("OnFrame") + + assertNear(baseMax * 1.25 * 1.4, build.calcsTab.calcsOutput.LightningMax, "more + increased max") + assertNear(baseMin * 0.5 * 2, build.calcsTab.calcsOutput.LightningMin, "less + increased min") + end) + + it("parses universal cannot deal/deal no non- damage for player and minions", function() + build.skillsTab:PasteSocketGroup("Slot: Body Armour\nArc 20/0 1\n") + runCallback("OnFrame") + assert.is_true(build.calcsTab.calcsOutput.LightningMax > 0) + + build.itemsTab:CreateDisplayItemFromRaw([[ + New Item + Coral Amulet + Cannot deal non-Fire Damage + Minions deal no non-Fire Damage + ]]) + build.itemsTab:AddDisplayItem() + runCallback("OnFrame") + + local modDB = build.calcsTab.mainEnv.player.modDB + assert.truthy(modDB:Flag(nil, "DealNoPhysical")) + assert.truthy(modDB:Flag(nil, "DealNoLightning")) + assert.truthy(modDB:Flag(nil, "DealNoCold")) + assert.truthy(modDB:Flag(nil, "DealNoChaos")) + assert.is_true(not (modDB:Flag(nil, "DealNoFire"))) + + -- minion wording wraps the same flags in MinionModifier + local minionDealNo = { } + for _, value in ipairs(modDB:List(nil, "MinionModifier")) do + if value.mod and value.mod.name:match("^DealNo") then + minionDealNo[value.mod.name] = true + end + end + assert.truthy(minionDealNo["DealNoPhysical"]) + assert.truthy(minionDealNo["DealNoLightning"]) + assert.truthy(minionDealNo["DealNoCold"]) + assert.truthy(minionDealNo["DealNoChaos"]) + assert.is_true(not (minionDealNo["DealNoFire"])) + + -- Arc is pure lightning, so the player can no longer deal damage with it + assert.are.equals(0, build.calcsTab.calcsOutput.LightningMax) + end) + + it("keeps deal no non-elemental damage as its own literal", function() + build.itemsTab:CreateDisplayItemFromRaw([[ + New Item + Coral Amulet + Deal no non-Elemental Damage + ]]) + build.itemsTab:AddDisplayItem() + runCallback("OnFrame") + + local modDB = build.calcsTab.mainEnv.player.modDB + assert.truthy(modDB:Flag(nil, "DealNoPhysical")) + assert.truthy(modDB:Flag(nil, "DealNoChaos")) + assert.is_true(not (modDB:Flag(nil, "DealNoLightning"))) + assert.is_true(not (modDB:Flag(nil, "DealNoCold"))) + assert.is_true(not (modDB:Flag(nil, "DealNoFire"))) + end) +end) diff --git a/src/Modules/CalcOffence.lua b/src/Modules/CalcOffence.lua index 673001eba7..1931c4c64a 100644 --- a/src/Modules/CalcOffence.lua +++ b/src/Modules/CalcOffence.lua @@ -115,6 +115,8 @@ local function calcDamage(activeSkill, output, cfg, breakdown, damageType, typeF local genericMoreMaxDamage = skillModList:More(cfg, "MaxDamage") local moreMinDamage = skillModList:More(cfg, "Min"..damageType.."Damage") local moreMaxDamage = skillModList:More(cfg, "Max"..damageType.."Damage") + local incMinDamage = 1 + skillModList:Sum("INC", cfg, "Min"..damageType.."Damage") / 100 + local incMaxDamage = 1 + skillModList:Sum("INC", cfg, "Max"..damageType.."Damage") / 100 if breakdown then t_insert(breakdown.damageTypes, { @@ -129,8 +131,8 @@ local function calcDamage(activeSkill, output, cfg, breakdown, damageType, typeF }) end - return round(((baseMin * inc * more) * genericMoreMinDamage + addMin) * moreMinDamage), - round(((baseMax * inc * more) * genericMoreMaxDamage + addMax) * moreMaxDamage) + return round(((baseMin * inc * more) * genericMoreMinDamage + addMin) * moreMinDamage * incMinDamage), + round(((baseMax * inc * more) * genericMoreMaxDamage + addMax) * moreMaxDamage * incMaxDamage) end local function calcAilmentSourceDamage(activeSkill, output, cfg, breakdown, damageType, typeFlags) diff --git a/src/Modules/ModParser.lua b/src/Modules/ModParser.lua index bc4a0ccf00..f434ba2724 100644 --- a/src/Modules/ModParser.lua +++ b/src/Modules/ModParser.lua @@ -1977,6 +1977,26 @@ local function flag(name, ...) return mod(name, "FLAG", true, ...) end +-- Returns the DealNo flags for every damage type except the given one, for the universal +-- "deal no non- damage" / "cannot deal non- damage" handlers. +-- "elemental" is not a single type and keeps its own literal entry; unknown types return nil +-- so the line falls through as unsupported instead of wrongly disabling all damage. +local dealNoNonDamageTypeList = { "Physical", "Lightning", "Cold", "Fire", "Chaos" } +local function dealNoNonDamageType(dmgType) + local keep = firstToUpper(dmgType) + local flags = { } + for _, t in ipairs(dealNoNonDamageTypeList) do + if t ~= keep then + t_insert(flags, flag("DealNo"..t)) + end + end + if #flags == #dealNoNonDamageTypeList then + -- dmgType didn't match any known damage type + return nil + end + return flags +end + local gemIdLookup = { ["power charge on critical strike"] = "SupportPowerChargeOnCritical", } @@ -2192,7 +2212,6 @@ local specialModList = { ["life leeched per second is doubled"] = { mod("LifeLeechRate", "MORE", 100) }, ["life regeneration has no effect"] = { flag("NoLifeRegen") }, ["energy shield recharge instead applies to life"] = { flag("EnergyShieldRechargeAppliesToLife") }, - ["deal no non%-fire damage"] = { flag("DealNoPhysical"), flag("DealNoLightning"), flag("DealNoCold"), flag("DealNoChaos") }, ["blade vortex and blade blast deal no non%-physical damage"] = { flag("DealNoLightning", { type = "SkillName", skillNameList = { "Blade Vortex", "Blade Blast" }, includeTransfigured = true }), flag("DealNoCold", { type = "SkillName", skillNameList = { "Blade Vortex", "Blade Blast" }, includeTransfigured = true }), @@ -4306,36 +4325,17 @@ local specialModList = { mod("MinionModifier", "LIST", { mod = flag("Condition:DiamondShrine") }, { type = "SkillName", skillName = "Summon Phantasm" }), mod("MinionModifier", "LIST", { mod = flag("Condition:MassiveShrine") }, { type = "SkillName", skillName = "Summon Phantasm" }), }, - ["minions deal no non%-physical damage"] = { - mod("MinionModifier", "LIST", { mod = flag("DealNoLightning") }), - mod("MinionModifier", "LIST", { mod = flag("DealNoCold") }), - mod("MinionModifier", "LIST", { mod = flag("DealNoFire") }), - mod("MinionModifier", "LIST", { mod = flag("DealNoChaos") }), - }, - ["minions deal no non%-lightning damage"] = { - mod("MinionModifier", "LIST", { mod = flag("DealNoPhysical") }), - mod("MinionModifier", "LIST", { mod = flag("DealNoLCold") }), - mod("MinionModifier", "LIST", { mod = flag("DealNoFire") }), - mod("MinionModifier", "LIST", { mod = flag("DealNoChaos") }), - }, - ["minions deal no non%-cold damage"] = { - mod("MinionModifier", "LIST", { mod = flag("DealNoPhysical") }), - mod("MinionModifier", "LIST", { mod = flag("DealNoLightning") }), - mod("MinionModifier", "LIST", { mod = flag("DealNoFire") }), - mod("MinionModifier", "LIST", { mod = flag("DealNoChaos") }), - }, - ["minions deal no non%-fire damage"] = { - mod("MinionModifier", "LIST", { mod = flag("DealNoPhysical") }), - mod("MinionModifier", "LIST", { mod = flag("DealNoLightning") }), - mod("MinionModifier", "LIST", { mod = flag("DealNoCold") }), - mod("MinionModifier", "LIST", { mod = flag("DealNoChaos") }), - }, - ["minions deal no non%-chaos damage"] = { - mod("MinionModifier", "LIST", { mod = flag("DealNoPhysical") }), - mod("MinionModifier", "LIST", { mod = flag("DealNoLightning") }), - mod("MinionModifier", "LIST", { mod = flag("DealNoCold") }), - mod("MinionModifier", "LIST", { mod = flag("DealNoFire") }), - }, + ["minions deal no non%-(%a+) damage"] = function(_, dmgType) + local flags = dealNoNonDamageType(dmgType) + if not flags then + return nil + end + local mods = { } + for _, dealNoFlag in ipairs(flags) do + t_insert(mods, mod("MinionModifier", "LIST", { mod = dealNoFlag })) + end + return mods + end, ["minions convert (%d+)%% of (.+) damage to (.+) damage"] = function(num, _, source, target) return { mod("MinionModifier", "LIST", { mod = mod(source:gsub("^%l", string.upper) .. "DamageConvertTo" .. target:gsub("^%l", string.upper), "BASE", num) }) } end, @@ -5141,9 +5141,8 @@ local specialModList = { flag("DealNoDamage", { type = "SkillType", skillTypeList = { SkillType.SummonsTotem, SkillType.RemoteMined, SkillType.Trapped}, neg = true }, {type = "Condition", var="usedByMirage", neg = true}), }, ["deal no non%-elemental damage"] = { flag("DealNoPhysical"), flag("DealNoChaos") }, - ["deal no non%-lightning damage"] = { flag("DealNoPhysical"), flag("DealNoCold"), flag("DealNoFire"), flag("DealNoChaos") }, - ["deal no non%-physical damage"] = { flag("DealNoLightning"), flag("DealNoCold"), flag("DealNoFire"), flag("DealNoChaos") }, - ["cannot deal non%-chaos damage"] = { flag("DealNoPhysical"), flag("DealNoCold"), flag("DealNoFire"), flag("DealNoLightning") }, + ["deal no non%-(%a+) damage"] = function(_, dmgType) return dealNoNonDamageType(dmgType) end, + ["cannot deal non%-(%a+) damage"] = function(_, dmgType) return dealNoNonDamageType(dmgType) end, ["deal no physical or elemental damage"] = { flag("DealNoPhysical"), flag("DealNoCold"), flag("DealNoFire"), flag("DealNoLightning") }, ["deal no damage when not on low life"] = { flag("DealNoDamage", { type = "Condition", var = "LowLife", neg = true }) }, ["spell skills deal no damage"] = { flag("DealNoDamage", { type = "SkillType", skillType = SkillType.Spell }) }, @@ -5270,6 +5269,22 @@ local specialModList = { ["attacks with this weapon have added maximum lightning damage equal to (%d+)%% of player'?s? maximum energy shield"] = function(num) return { mod("LightningMax", "BASE", 1, { type = "PercentStat", stat = "EnergyShield" , percent = num, actor = "parent" }, { type = "Condition", var = "{Hand}Attack" }, { type = "SkillType", skillType = SkillType.Attack }), } end, + -- Scaling the minimum/maximum roll of a damage type (covers all types, e.g. "maximum lightning damage", "minimum cold damage"). + -- `(m[ia][xn]imum)` captures the literal words "maximum" or "minimum". These map to the MinDamage/MaxDamage stats + -- consumed by calcDamage in CalcOffence. "more"/"less" apply as MORE: + ["(%d+)%% more (m[ia][xn]imum) (%a+) damage"] = function(num, _, minMax, dmgType) return { + mod((minMax == "maximum" and "Max" or "Min")..firstToUpper(dmgType).."Damage", "MORE", num), + } end, + ["(%d+)%% less (m[ia][xn]imum) (%a+) damage"] = function(num, _, minMax, dmgType) return { + mod((minMax == "maximum" and "Max" or "Min")..firstToUpper(dmgType).."Damage", "MORE", -num), + } end, + -- ...while "increased"/"reduced" apply as INC: + ["(%d+)%% increased (m[ia][xn]imum) (%a+) damage"] = function(num, _, minMax, dmgType) return { + mod((minMax == "maximum" and "Max" or "Min")..firstToUpper(dmgType).."Damage", "INC", num), + } end, + ["(%d+)%% reduced (m[ia][xn]imum) (%a+) damage"] = function(num, _, minMax, dmgType) return { + mod((minMax == "maximum" and "Max" or "Min")..firstToUpper(dmgType).."Damage", "INC", -num), + } end, ["adds (%d+)%% of your maximum mana as fire damage to attacks with this weapon"] = function(num) return { mod("FireMin", "BASE", 1, { type = "PercentStat", stat = "Mana" , percent = num }, { type = "Condition", var = "{Hand}Attack" }, { type = "SkillType", skillType = SkillType.Attack }), mod("FireMax", "BASE", 1, { type = "PercentStat", stat = "Mana" , percent = num }, { type = "Condition", var = "{Hand}Attack" }, { type = "SkillType", skillType = SkillType.Attack }), From 162db998e6df8dda4673322324dfd2ad1e57bf98 Mon Sep 17 00:00:00 2001 From: SaloEater Date: Wed, 22 Jul 2026 04:14:55 -0500 Subject: [PATCH 2/9] feature: Bitter Frost notable --- spec/System/TestOffence_spec.lua | 37 +++++++++++++++++++++++++++++++- src/Data/ModCache.lua | 2 +- src/Modules/CalcPerform.lua | 11 ++++++++++ src/Modules/ModParser.lua | 1 + 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/spec/System/TestOffence_spec.lua b/spec/System/TestOffence_spec.lua index c96b4b2d41..0dbf7ace0b 100644 --- a/spec/System/TestOffence_spec.lua +++ b/spec/System/TestOffence_spec.lua @@ -1,4 +1,4 @@ -describe("TestMinMaxDamageMods", function() +describe("TestOffence", function() before_each(function() newBuild() end) @@ -7,6 +7,12 @@ describe("TestMinMaxDamageMods", function() -- newBuild() takes care of resetting everything in setup() end) + -- Asserts actual is within a relative tolerance of expected, e.g. 0.005 = 0.5% + local function assertNearRelative(expected, actual, tolerance, msg) + assert.is_true(math.abs(expected - actual) / expected <= tolerance, + string.format("%s: expected ~%.2f (within %.1f%%), got %.2f", msg, expected, tolerance * 100, actual)) + end + it("parses more/less/increased/reduced minimum and maximum damage of every type", function() build.itemsTab:CreateDisplayItemFromRaw([[ New Item @@ -134,4 +140,33 @@ describe("TestMinMaxDamageMods", function() assert.is_true(not (modDB:Flag(nil, "DealNoCold"))) assert.is_true(not (modDB:Flag(nil, "DealNoFire"))) end) + + it("enemies in your chilling areas take damage increased by the area's chill effect", function() + build.skillsTab:PasteSocketGroup("Slot: Body Armour\nVortex 20/0 1\n") + runCallback("OnFrame") + + local baseAvg = build.calcsTab.mainOutput.AverageDamage + assert.is_true(baseAvg > 0) + -- the chilling area's own chill effect, as computed by CalcOffence + local areaChill = build.calcsTab.mainOutput.ChillSourceEffect + assert.is_true(areaChill ~= nil and areaChill > 0) + + build.itemsTab:CreateDisplayItemFromRaw([[ + New Item + Coral Amulet + Enemies in your Chilling Areas have Damage taken increased by Chill Effect + ]]) + build.itemsTab:AddDisplayItem() + runCallback("OnFrame") + + -- config checkbox not ticked -> enemy is not in the area -> no change + assert.are.equals(baseAvg, build.calcsTab.mainOutput.AverageDamage) + + build.configTab.input.conditionEnemyInChillingArea = true + build.configTab:BuildModList() + runCallback("OnFrame") + + assertNearRelative(baseAvg * (1 + areaChill / 100), build.calcsTab.mainOutput.AverageDamage, 0.005, + string.format("base %.2f scaled by %d%% area chill", baseAvg, areaChill)) + end) end) diff --git a/src/Data/ModCache.lua b/src/Data/ModCache.lua index 18082b7621..3223dc0908 100755 --- a/src/Data/ModCache.lua +++ b/src/Data/ModCache.lua @@ -7886,7 +7886,7 @@ c["Cannot be Stunned while you have at least 25 Rage"]={{[1]={[1]={threshold=25, c["Cannot be used with Chaos Inoculation"]={nil,"Cannot be used with Chaos Inoculation "} c["Cannot be used with Chaos Inoculation Reserves 30% of Life"]={nil,"Cannot be used with Chaos Inoculation Reserves 30% of Life "} c["Cannot deal Critical Strikes with Attacks"]={{[1]={flags=1,keywordFlags=0,name="NeverCrit",type="FLAG",value=true},[2]={flags=1,keywordFlags=0,name="Condition:NeverCrit",type="FLAG",value=true}},nil} -c["Cannot deal non-Chaos Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true}},nil} +c["Cannot deal non-Chaos Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true}},nil} c["Cannot gain Energy Shield"]={{[1]={flags=0,keywordFlags=0,name="CannotGainEnergyShield",type="FLAG",value=true}},nil} c["Cannot gain Life during effect"]={{[1]={[1]={type="Condition",var="UsingFlask"},flags=0,keywordFlags=0,name="CannotGainLife",type="FLAG",value=true}},nil} c["Cannot gain Mana during effect"]={{[1]={[1]={type="Condition",var="UsingFlask"},flags=0,keywordFlags=0,name="CannotGainMana",type="FLAG",value=true}},nil} diff --git a/src/Modules/CalcPerform.lua b/src/Modules/CalcPerform.lua index 1fb88582c4..270bb6d49b 100644 --- a/src/Modules/CalcPerform.lua +++ b/src/Modules/CalcPerform.lua @@ -1227,6 +1227,7 @@ function calcs.perform(env, skipEHP) end local hasGuaranteedBonechill = false + local chillingAreaChillEffect = 0 -- Banners if modDB:Flag(nil,"Condition:BannerPlanted") then @@ -1325,6 +1326,9 @@ function calcs.perform(env, skipEHP) local effect = data.nonDamagingAilment.Chill.default * calcLib.mod(activeSkill.skillModList, activeSkill.skillCfg, "EnemyChillEffect") modDB:NewMod("ChillOverride", "BASE", effect, activeSkill.activeEffect.grantedEffect.name) enemyDB:NewMod("Condition:Chilled", "FLAG", true, activeSkill.activeEffect.grantedEffect.name) + if activeSkill.skillTypes[SkillType.ChillingArea] then + chillingAreaChillEffect = m_max(chillingAreaChillEffect, effect) + end if activeSkill.skillData.supportBonechill then hasGuaranteedBonechill = true end @@ -3352,6 +3356,13 @@ function calcs.perform(env, skipEHP) elseif output.HasBonechill and (hasGuaranteedBonechill or enemyDB:Sum("BASE", nil, "ChillVal") > 0) then t_insert(mods, modLib.createMod("ColdDamageTaken", "INC", num, "Bonechill", { type = "Condition", var = "Chilled" })) end + -- Stacks with the above as a separate increase; scales off the chilling area's own + -- chill effect rather than the strongest chill applied to the enemy. + -- The flag itself is gated on the enemy being in a chilling area (config option). + if modDB:Flag(nil, "ChillingAreaIncDamageTaken") and chillingAreaChillEffect > 0 then + local areaChill = m_floor(m_min(chillingAreaChillEffect, output.MaximumChill) * (10 ^ ailmentData.Chill.precision)) / (10 ^ ailmentData.Chill.precision) + t_insert(mods, modLib.createMod("DamageTaken", "INC", areaChill, "Chilling Area", { type = "Condition", var = "Chilled" })) + end if modDB:Flag(nil, "ChillEffectLessDamageDealt") then t_insert(mods, modLib.createMod("Damage", "MORE", -num / 2, "Shaper of Winter", { type = "Condition", var = "Chilled" })) end diff --git a/src/Modules/ModParser.lua b/src/Modules/ModParser.lua index f434ba2724..73b3266564 100644 --- a/src/Modules/ModParser.lua +++ b/src/Modules/ModParser.lua @@ -3719,6 +3719,7 @@ local specialModList = { flag("SpellSuppressionAppliesToChanceToDefendWithArmour"), } end, ["enemies chilled by your hits have damage taken increased by chill effect"] = { flag("ChillEffectIncDamageTaken") }, + ["enemies in your chilling areas have damage taken increased by chill effect"] = { flag("ChillingAreaIncDamageTaken", { type = "ActorCondition", actor = "enemy", var = "InChillingArea" }) }, ["left ring slot: your chilling skitterbot's aura applies socketed h?e?x? ?curse instead"] = { flag("SkitterbotsCannotChill", { type = "SlotNumber", num = 1 }) }, ["right ring slot: your shocking skitterbot's aura applies socketed h?e?x? ?curse instead"] = { flag("SkitterbotsCannotShock", { type = "SlotNumber", num = 2 }) }, ["summon skitterbots also summons a scorching skitterbot"] = { flag("ScorchingSkitterbot") }, From ab605b5c3548798d59c76c0f5c8e4a5e159018f4 Mon Sep 17 00:00:00 2001 From: SaloEater Date: Wed, 22 Jul 2026 04:32:33 -0500 Subject: [PATCH 3/9] feature: Voracious Flame notable --- spec/System/TestOffence_spec.lua | 34 ++++++++++++++++++++++++++++++++ src/Modules/CalcOffence.lua | 11 +++-------- src/Modules/ModParser.lua | 9 +++++++++ 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/spec/System/TestOffence_spec.lua b/spec/System/TestOffence_spec.lua index 0dbf7ace0b..e2f40ab528 100644 --- a/spec/System/TestOffence_spec.lua +++ b/spec/System/TestOffence_spec.lua @@ -169,4 +169,38 @@ describe("TestOffence", function() assertNearRelative(baseAvg * (1 + areaChill / 100), build.calcsTab.mainOutput.AverageDamage, 0.005, string.format("base %.2f scaled by %d%% area chill", baseAvg, areaChill)) end) + + -- "Base Duration is X seconds" overrides the fixed base duration of the damaging ailments + for _, case in ipairs({ + { ailment = "Ignite", skill = "Fireball", chanceMod = "25% chance to Ignite", gameBase = 4 }, + { ailment = "Bleeding", skill = "Double Strike", chanceMod = "25% chance to cause Bleeding on Hit", gameBase = 5, output = "BleedDuration" }, + { ailment = "Poison", skill = "Double Strike", chanceMod = "25% chance to Poison on Hit", gameBase = 2 }, + }) do + it("supports base " .. case.ailment .. " duration override", function() + local outputName = case.output or (case.ailment .. "Duration") + build.itemsTab:CreateDisplayItemFromRaw([[ + New Item + Rusted Sword + ]]) + build.itemsTab:AddDisplayItem() + build.skillsTab:PasteSocketGroup("Slot: Body Armour\n" .. case.skill .. " 20/0 1\n") + build.itemsTab:CreateDisplayItemFromRaw([[ + New Item + Coral Amulet + ]] .. case.chanceMod) + build.itemsTab:AddDisplayItem() + runCallback("OnFrame") + + assert.are.equals(case.gameBase, build.calcsTab.mainOutput[outputName]) + + build.itemsTab:CreateDisplayItemFromRaw([[ + New Item + Coral Ring + Base ]] .. case.ailment .. [[ Duration is 1 second]]) + build.itemsTab:AddDisplayItem() + runCallback("OnFrame") + + assert.are.equals(1, build.calcsTab.mainOutput[outputName]) + end) + end end) diff --git a/src/Modules/CalcOffence.lua b/src/Modules/CalcOffence.lua index 1931c4c64a..4eb17b5234 100644 --- a/src/Modules/CalcOffence.lua +++ b/src/Modules/CalcOffence.lua @@ -4203,7 +4203,7 @@ function calcs.offence(env, actor, activeSkill) local maxStacks = skillModList:Override(cfg, "BleedStacksMax") or skillModList:Sum("BASE", cfg, "BleedStacksMax") local overrideStackPotential = skillModList:Override(nil, "BleedStackPotentialOverride") and skillModList:Override(nil, "BleedStackPotentialOverride") / maxStacks globalOutput.BleedStacksMax = maxStacks - local durationBase = skillData.bleedDurationIsSkillDuration and skillData.duration or data.misc.BleedDurationBase + local durationBase = skillModList:Override(dotCfg, "BleedDurationBase") or (skillData.bleedDurationIsSkillDuration and skillData.duration) or data.misc.BleedDurationBase local durationMod = calcLib.mod(skillModList, dotCfg, "EnemyBleedDuration", "EnemyAilmentDuration", "DamagingAilmentDuration", skillData.bleedIsSkillEffect and "Duration" or nil) * calcLib.mod(enemyDB, nil, "SelfBleedDuration", "SelfAilmentDuration") / calcLib.mod(enemyDB, dotCfg, "BleedExpireRate") durationMod = m_max(durationMod, 0) local rateMod = calcLib.mod(skillModList, cfg, "BleedFaster") + enemyDB:Sum("INC", nil, "SelfBleedFaster") / 100 @@ -4475,12 +4475,7 @@ function calcs.offence(env, actor, activeSkill) breakdown.PoisonChaos = { damageTypes = { } } end local rateMod = calcLib.mod(skillModList, cfg, "PoisonFaster") + enemyDB:Sum("INC", nil, "SelfPoisonFaster") / 100 - local durationBase - if skillData.poisonDurationIsSkillDuration then - durationBase = skillData.duration - else - durationBase = data.misc.PoisonDurationBase - end + local durationBase = skillModList:Override(dotCfg, "PoisonDurationBase") or (skillData.poisonDurationIsSkillDuration and skillData.duration) or data.misc.PoisonDurationBase local durationMod = calcLib.mod(skillModList, dotCfg, "EnemyPoisonDuration", "EnemyAilmentDuration", "DamagingAilmentDuration", skillData.poisonIsSkillEffect and "Duration" or nil) * calcLib.mod(enemyDB, nil, "SelfPoisonDuration", "SelfAilmentDuration") durationMod = m_max(durationMod, 0) globalOutput.PoisonDuration = durationBase * durationMod / rateMod * debuffDurationMult @@ -4796,7 +4791,7 @@ function calcs.offence(env, actor, activeSkill) globalOutput.IgniteStacksMax = maxStacks local rateMod = (calcLib.mod(skillModList, cfg, "IgniteBurnFaster") + enemyDB:Sum("INC", nil, "SelfIgniteBurnFaster") / 100) / calcLib.mod(skillModList, cfg, "IgniteBurnSlower") - local durationBase = data.misc.IgniteDurationBase + local durationBase = skillModList:Override(dotCfg, "IgniteDurationBase") or data.misc.IgniteDurationBase local durationMod = m_max(calcLib.mod(skillModList, dotCfg, "EnemyIgniteDuration", "EnemyAilmentDuration", "EnemyElementalAilmentDuration", "DamagingAilmentDuration") * calcLib.mod(enemyDB, nil, "SelfIgniteDuration", "SelfAilmentDuration", "SelfElementalAilmentDuration"), 0) durationMod = m_max(durationMod, 0) globalOutput.IgniteDuration = durationBase * durationMod / rateMod * debuffDurationMult diff --git a/src/Modules/ModParser.lua b/src/Modules/ModParser.lua index 73b3266564..908dea8dd9 100644 --- a/src/Modules/ModParser.lua +++ b/src/Modules/ModParser.lua @@ -3570,6 +3570,15 @@ local specialModList = { mod("Damage", "INC", num, nil, 0, KeywordFlag.Poison, { type = "ActorCondition", actor = "enemy", var = "Poisoned" }, { type = "Condition", var = "Poisoned" }), } end, ["ignited enemies burn (%d+)%% faster"] = function(num) return { mod("IgniteBurnFaster", "INC", num) } end, + -- Overrides the base duration of a damaging ailment (the only ailments with a fixed base duration + -- consumed by the calcs; freeze duration is derived from damage and chill/shock durations are not modelled) + ["base (%a+) duration is ([%d%.]+) seconds?"] = function(_, ailment, num) + local ailmentName = (ailment == "bleeding" or ailment == "bleed") and "Bleed" + or (ailment == "ignite" or ailment == "poison") and firstToUpper(ailment) + if ailmentName then + return { mod(ailmentName.."DurationBase", "OVERRIDE", tonumber(num)) } + end + end, ["ignited enemies burn (%d+)%% slower"] = function(num) return { mod("IgniteBurnSlower", "INC", num) } end, ["enemies ignited by an attack burn (%d+)%% faster"] = function(num) return { mod("IgniteBurnFaster", "INC", num, nil, ModFlag.Attack) } end, ["ignites you inflict with attacks deal damage (%d+)%% faster"] = function(num) return { mod("IgniteBurnFaster", "INC", num, nil, ModFlag.Attack) } end, From 641eea611766d7feab4e31e63edec122eb97205b Mon Sep 17 00:00:00 2001 From: SaloEater Date: Wed, 22 Jul 2026 04:40:47 -0500 Subject: [PATCH 4/9] chore: test order --- src/Modules/ModParser.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Modules/ModParser.lua b/src/Modules/ModParser.lua index 908dea8dd9..1f19f7714d 100644 --- a/src/Modules/ModParser.lua +++ b/src/Modules/ModParser.lua @@ -1981,7 +1981,7 @@ end -- "deal no non- damage" / "cannot deal non- damage" handlers. -- "elemental" is not a single type and keeps its own literal entry; unknown types return nil -- so the line falls through as unsupported instead of wrongly disabling all damage. -local dealNoNonDamageTypeList = { "Physical", "Lightning", "Cold", "Fire", "Chaos" } +local dealNoNonDamageTypeList = { "Physical", "Cold", "Fire", "Lightning", "Chaos" } local function dealNoNonDamageType(dmgType) local keep = firstToUpper(dmgType) local flags = { } From a582fb197342a33ffca3a0efb01687a8de10fc61 Mon Sep 17 00:00:00 2001 From: SaloEater Date: Wed, 22 Jul 2026 04:49:59 -0500 Subject: [PATCH 5/9] fix: calcs --- spec/System/TestOffence_spec.lua | 10 +++++----- src/Modules/CalcPerform.lua | 15 ++++----------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/spec/System/TestOffence_spec.lua b/spec/System/TestOffence_spec.lua index e2f40ab528..71cb61944e 100644 --- a/spec/System/TestOffence_spec.lua +++ b/spec/System/TestOffence_spec.lua @@ -147,9 +147,9 @@ describe("TestOffence", function() local baseAvg = build.calcsTab.mainOutput.AverageDamage assert.is_true(baseAvg > 0) - -- the chilling area's own chill effect, as computed by CalcOffence - local areaChill = build.calcsTab.mainOutput.ChillSourceEffect - assert.is_true(areaChill ~= nil and areaChill > 0) + -- the chill effect currently applied to the enemy (here, from Vortex's chilling area) + local currentChill = build.calcsTab.mainOutput.CurrentChill + assert.is_true(currentChill ~= nil and currentChill > 0) build.itemsTab:CreateDisplayItemFromRaw([[ New Item @@ -166,8 +166,8 @@ describe("TestOffence", function() build.configTab:BuildModList() runCallback("OnFrame") - assertNearRelative(baseAvg * (1 + areaChill / 100), build.calcsTab.mainOutput.AverageDamage, 0.005, - string.format("base %.2f scaled by %d%% area chill", baseAvg, areaChill)) + assertNearRelative(baseAvg * (1 + currentChill / 100), build.calcsTab.mainOutput.AverageDamage, 0.005, + string.format("base %.2f scaled by %d%% current chill", baseAvg, currentChill)) end) -- "Base Duration is X seconds" overrides the fixed base duration of the damaging ailments diff --git a/src/Modules/CalcPerform.lua b/src/Modules/CalcPerform.lua index 270bb6d49b..b3f17d5869 100644 --- a/src/Modules/CalcPerform.lua +++ b/src/Modules/CalcPerform.lua @@ -1227,7 +1227,6 @@ function calcs.perform(env, skipEHP) end local hasGuaranteedBonechill = false - local chillingAreaChillEffect = 0 -- Banners if modDB:Flag(nil,"Condition:BannerPlanted") then @@ -1326,9 +1325,6 @@ function calcs.perform(env, skipEHP) local effect = data.nonDamagingAilment.Chill.default * calcLib.mod(activeSkill.skillModList, activeSkill.skillCfg, "EnemyChillEffect") modDB:NewMod("ChillOverride", "BASE", effect, activeSkill.activeEffect.grantedEffect.name) enemyDB:NewMod("Condition:Chilled", "FLAG", true, activeSkill.activeEffect.grantedEffect.name) - if activeSkill.skillTypes[SkillType.ChillingArea] then - chillingAreaChillEffect = m_max(chillingAreaChillEffect, effect) - end if activeSkill.skillData.supportBonechill then hasGuaranteedBonechill = true end @@ -3353,16 +3349,13 @@ function calcs.perform(env, skipEHP) local mods = { modLib.createMod("ActionSpeed", "INC", -num, "Chill", { type = "Condition", var = "Chilled" }) } if modDB:Flag(nil, "ChillEffectIncDamageTaken") then t_insert(mods, modLib.createMod("DamageTaken", "INC", num, "Ahuana's Bite", { type = "Condition", var = "Chilled" })) + -- Scales off the current chill effect on the enemy; the flag itself is gated on the + -- enemy being in a chilling area (config option) + elseif modDB:Flag(nil, "ChillingAreaIncDamageTaken") then + t_insert(mods, modLib.createMod("DamageTaken", "INC", num, "Chilling Area", { type = "Condition", var = "Chilled" })) elseif output.HasBonechill and (hasGuaranteedBonechill or enemyDB:Sum("BASE", nil, "ChillVal") > 0) then t_insert(mods, modLib.createMod("ColdDamageTaken", "INC", num, "Bonechill", { type = "Condition", var = "Chilled" })) end - -- Stacks with the above as a separate increase; scales off the chilling area's own - -- chill effect rather than the strongest chill applied to the enemy. - -- The flag itself is gated on the enemy being in a chilling area (config option). - if modDB:Flag(nil, "ChillingAreaIncDamageTaken") and chillingAreaChillEffect > 0 then - local areaChill = m_floor(m_min(chillingAreaChillEffect, output.MaximumChill) * (10 ^ ailmentData.Chill.precision)) / (10 ^ ailmentData.Chill.precision) - t_insert(mods, modLib.createMod("DamageTaken", "INC", areaChill, "Chilling Area", { type = "Condition", var = "Chilled" })) - end if modDB:Flag(nil, "ChillEffectLessDamageDealt") then t_insert(mods, modLib.createMod("Damage", "MORE", -num / 2, "Shaper of Winter", { type = "Condition", var = "Chilled" })) end From 397a33543e5cea6ab818a8d6527e081f45b17f4c Mon Sep 17 00:00:00 2001 From: SaloEater Date: Wed, 22 Jul 2026 04:55:00 -0500 Subject: [PATCH 6/9] chore: pipeline --- src/Data/ModCache.lua | 8 ++++---- src/Modules/ModParser.lua | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Data/ModCache.lua b/src/Data/ModCache.lua index 3223dc0908..10b50d0199 100755 --- a/src/Data/ModCache.lua +++ b/src/Data/ModCache.lua @@ -7886,7 +7886,7 @@ c["Cannot be Stunned while you have at least 25 Rage"]={{[1]={[1]={threshold=25, c["Cannot be used with Chaos Inoculation"]={nil,"Cannot be used with Chaos Inoculation "} c["Cannot be used with Chaos Inoculation Reserves 30% of Life"]={nil,"Cannot be used with Chaos Inoculation Reserves 30% of Life "} c["Cannot deal Critical Strikes with Attacks"]={{[1]={flags=1,keywordFlags=0,name="NeverCrit",type="FLAG",value=true},[2]={flags=1,keywordFlags=0,name="Condition:NeverCrit",type="FLAG",value=true}},nil} -c["Cannot deal non-Chaos Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true}},nil} +c["Cannot deal non-Chaos Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true}},nil} c["Cannot gain Energy Shield"]={{[1]={flags=0,keywordFlags=0,name="CannotGainEnergyShield",type="FLAG",value=true}},nil} c["Cannot gain Life during effect"]={{[1]={[1]={type="Condition",var="UsingFlask"},flags=0,keywordFlags=0,name="CannotGainLife",type="FLAG",value=true}},nil} c["Cannot gain Mana during effect"]={{[1]={[1]={type="Condition",var="UsingFlask"},flags=0,keywordFlags=0,name="CannotGainMana",type="FLAG",value=true}},nil} @@ -8230,9 +8230,9 @@ c["Deal no Cold Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoCold",type="F c["Deal no Damage when not on Low Life"]={{[1]={[1]={neg=true,type="Condition",var="LowLife"},flags=0,keywordFlags=0,name="DealNoDamage",type="FLAG",value=true}},nil} c["Deal no Elemental Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true}},nil} c["Deal no Non-Elemental Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}},nil} -c["Deal no Non-Fire Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}},nil} +c["Deal no Non-Fire Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}},nil} c["Deal no Non-Lightning Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}},nil} -c["Deal no Non-Physical Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}},nil} +c["Deal no Non-Physical Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}},nil} c["Deal no Physical Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true}},nil} c["Deal no Physical or Elemental Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true}},nil} c["Deal up to 15% more Melee Damage to Enemies, based on proximity"]={{[1]={[1]={ramp={[1]=1,[2]=0},type="MeleeProximity"},flags=257,keywordFlags=0,name="Damage",type="MORE",value=15}},nil} @@ -9905,7 +9905,7 @@ c["Minions deal 80% increased Damage"]={{[1]={flags=0,keywordFlags=0,name="Minio c["Minions deal 80% increased Damage if you have Warcried Recently"]={{[1]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={[1]={actor="parent",type="ActorCondition",var="UsedWarcryRecently"},flags=0,keywordFlags=0,name="Damage",type="INC",value=80}}}},nil} c["Minions deal 9 to 15 additional Cold Damage"]={{[1]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="ColdMin",type="BASE",value=9}}},[2]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="ColdMax",type="BASE",value=15}}}},nil} c["Minions deal 96 to 144 additional Physical Damage"]={{[1]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="PhysicalMin",type="BASE",value=96}}},[2]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="PhysicalMax",type="BASE",value=144}}}},nil} -c["Minions deal no Non-Cold Damage"]={{[1]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true}}},[2]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true}}},[3]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true}}},[4]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}}}},nil} +c["Minions deal no Non-Cold Damage"]={{[1]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true}}},[2]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true}}},[3]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true}}},[4]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}}}},nil} c["Minions from Herald Skills deal 25% more Damage"]={{[1]={[1]={skillType=62,type="SkillType"},flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="Damage",type="MORE",value=25}}}},nil} c["Minions gain 18% of Elemental Damage as Extra Chaos Damage"]={{[1]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="ElementalDamageGainAsChaos",type="BASE",value=18}}}},nil} c["Minions gain 20% of Elemental Damage as Extra Chaos Damage"]={{[1]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="ElementalDamageGainAsChaos",type="BASE",value=20}}}},nil} diff --git a/src/Modules/ModParser.lua b/src/Modules/ModParser.lua index 1f19f7714d..f792a5994e 100644 --- a/src/Modules/ModParser.lua +++ b/src/Modules/ModParser.lua @@ -5279,6 +5279,7 @@ local specialModList = { ["attacks with this weapon have added maximum lightning damage equal to (%d+)%% of player'?s? maximum energy shield"] = function(num) return { mod("LightningMax", "BASE", 1, { type = "PercentStat", stat = "EnergyShield" , percent = num, actor = "parent" }, { type = "Condition", var = "{Hand}Attack" }, { type = "SkillType", skillType = SkillType.Attack }), } end, + -- cspell:ignore imum -- Scaling the minimum/maximum roll of a damage type (covers all types, e.g. "maximum lightning damage", "minimum cold damage"). -- `(m[ia][xn]imum)` captures the literal words "maximum" or "minimum". These map to the MinDamage/MaxDamage stats -- consumed by calcDamage in CalcOffence. "more"/"less" apply as MORE: From dd531433663f5fb62efa9db8cc30b571fd36eb2a Mon Sep 17 00:00:00 2001 From: SaloEater Date: Wed, 22 Jul 2026 05:06:17 -0500 Subject: [PATCH 7/9] fix: new wording --- spec/System/TestOffence_spec.lua | 18 ++++++++++++++++-- src/Modules/CalcPerform.lua | 6 ++++-- src/Modules/ModParser.lua | 3 ++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/spec/System/TestOffence_spec.lua b/spec/System/TestOffence_spec.lua index 71cb61944e..bb006262e1 100644 --- a/spec/System/TestOffence_spec.lua +++ b/spec/System/TestOffence_spec.lua @@ -154,7 +154,7 @@ describe("TestOffence", function() build.itemsTab:CreateDisplayItemFromRaw([[ New Item Coral Amulet - Enemies in your Chilling Areas have Damage taken increased by Chill Effect + Enemies in your Chilling Areas have Cold Damage taken increased by Chill Effect ]]) build.itemsTab:AddDisplayItem() runCallback("OnFrame") @@ -166,8 +166,22 @@ describe("TestOffence", function() build.configTab:BuildModList() runCallback("OnFrame") - assertNearRelative(baseAvg * (1 + currentChill / 100), build.calcsTab.mainOutput.AverageDamage, 0.005, + -- Vortex deals pure cold damage, so the ColdDamageTaken increase scales all of it + local scaledAvg = baseAvg * (1 + currentChill / 100) + assertNearRelative(scaledAvg, build.calcsTab.mainOutput.AverageDamage, 0.005, string.format("base %.2f scaled by %d%% current chill", baseAvg, currentChill)) + + -- the paired "Chilled by your Hits" wording must not stack with the chilling area one + build.itemsTab:CreateDisplayItemFromRaw([[ + New Item + Coral Ring + Enemies Chilled by your Hits have Cold Damage taken increased by Chill Effect + ]]) + build.itemsTab:AddDisplayItem() + runCallback("OnFrame") + + assertNearRelative(scaledAvg, build.calcsTab.mainOutput.AverageDamage, 0.005, + string.format("both wordings must apply only once (base %.2f, %d%% chill)", baseAvg, currentChill)) end) -- "Base Duration is X seconds" overrides the fixed base duration of the damaging ailments diff --git a/src/Modules/CalcPerform.lua b/src/Modules/CalcPerform.lua index b3f17d5869..e83bd64fbb 100644 --- a/src/Modules/CalcPerform.lua +++ b/src/Modules/CalcPerform.lua @@ -3349,10 +3349,12 @@ function calcs.perform(env, skipEHP) local mods = { modLib.createMod("ActionSpeed", "INC", -num, "Chill", { type = "Condition", var = "Chilled" }) } if modDB:Flag(nil, "ChillEffectIncDamageTaken") then t_insert(mods, modLib.createMod("DamageTaken", "INC", num, "Ahuana's Bite", { type = "Condition", var = "Chilled" })) + elseif modDB:Flag(nil, "ChillEffectIncColdDamageTaken") then + t_insert(mods, modLib.createMod("ColdDamageTaken", "INC", num, "Chilled by Hits", { type = "Condition", var = "Chilled" })) -- Scales off the current chill effect on the enemy; the flag itself is gated on the -- enemy being in a chilling area (config option) - elseif modDB:Flag(nil, "ChillingAreaIncDamageTaken") then - t_insert(mods, modLib.createMod("DamageTaken", "INC", num, "Chilling Area", { type = "Condition", var = "Chilled" })) + elseif modDB:Flag(nil, "ChillingAreaIncColdDamageTaken") then + t_insert(mods, modLib.createMod("ColdDamageTaken", "INC", num, "Chilling Area", { type = "Condition", var = "Chilled" })) elseif output.HasBonechill and (hasGuaranteedBonechill or enemyDB:Sum("BASE", nil, "ChillVal") > 0) then t_insert(mods, modLib.createMod("ColdDamageTaken", "INC", num, "Bonechill", { type = "Condition", var = "Chilled" })) end diff --git a/src/Modules/ModParser.lua b/src/Modules/ModParser.lua index f792a5994e..e063736854 100644 --- a/src/Modules/ModParser.lua +++ b/src/Modules/ModParser.lua @@ -3728,7 +3728,8 @@ local specialModList = { flag("SpellSuppressionAppliesToChanceToDefendWithArmour"), } end, ["enemies chilled by your hits have damage taken increased by chill effect"] = { flag("ChillEffectIncDamageTaken") }, - ["enemies in your chilling areas have damage taken increased by chill effect"] = { flag("ChillingAreaIncDamageTaken", { type = "ActorCondition", actor = "enemy", var = "InChillingArea" }) }, + ["enemies chilled by your hits have cold damage taken increased by chill effect"] = { flag("ChillEffectIncColdDamageTaken") }, + ["enemies in your chilling areas have cold damage taken increased by chill effect"] = { flag("ChillingAreaIncColdDamageTaken", { type = "ActorCondition", actor = "enemy", var = "InChillingArea" }) }, ["left ring slot: your chilling skitterbot's aura applies socketed h?e?x? ?curse instead"] = { flag("SkitterbotsCannotChill", { type = "SlotNumber", num = 1 }) }, ["right ring slot: your shocking skitterbot's aura applies socketed h?e?x? ?curse instead"] = { flag("SkitterbotsCannotShock", { type = "SlotNumber", num = 2 }) }, ["summon skitterbots also summons a scorching skitterbot"] = { flag("ScorchingSkitterbot") }, From 87a8d3340124e9f647dc60099a868d33dd053473 Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Fri, 24 Jul 2026 07:23:50 +1000 Subject: [PATCH 8/9] Simplify ModParser --- spec/System/TestOffence_spec.lua | 25 ++++++++++++ src/Data/ModCache.lua | 32 ++++++---------- src/Modules/ModParser.lua | 65 ++++++++++---------------------- 3 files changed, 57 insertions(+), 65 deletions(-) diff --git a/spec/System/TestOffence_spec.lua b/spec/System/TestOffence_spec.lua index bb006262e1..36b67ddc53 100644 --- a/spec/System/TestOffence_spec.lua +++ b/spec/System/TestOffence_spec.lua @@ -86,6 +86,31 @@ describe("TestOffence", function() assertNear(baseMin * 0.5 * 2, build.calcsTab.calcsOutput.LightningMin, "less + increased min") end) + it("applies minimum and maximum attack damage mods", function() + build.itemsTab:CreateDisplayItemFromRaw([[ + New Item + Rusted Sword + ]]) + build.itemsTab:AddDisplayItem() + build.skillsTab:PasteSocketGroup("Double Strike 20/0 1") + runCallback("OnFrame") + + local baseMin = build.calcsTab.mainOutput.MainHand.TotalMin + local baseMax = build.calcsTab.mainOutput.MainHand.TotalMax + + build.itemsTab:CreateDisplayItemFromRaw([[ + New Item + Coral Amulet + 50% less Minimum Attack Damage + 100% more Maximum Attack Damage + ]]) + build.itemsTab:AddDisplayItem() + runCallback("OnFrame") + + assertNear(baseMin * 0.5, build.calcsTab.mainOutput.MainHand.TotalMin, "50%% less attack min") + assertNear(baseMax * 2, build.calcsTab.mainOutput.MainHand.TotalMax, "100%% more attack max") + end) + it("parses universal cannot deal/deal no non- damage for player and minions", function() build.skillsTab:PasteSocketGroup("Slot: Body Armour\nArc 20/0 1\n") runCallback("OnFrame") diff --git a/src/Data/ModCache.lua b/src/Data/ModCache.lua index 790095357d..d1b966ee15 100755 --- a/src/Data/ModCache.lua +++ b/src/Data/ModCache.lua @@ -4510,9 +4510,7 @@ c["25% more Critical Strike chance while affected by Precision"]={{[1]={[1]={typ c["25% more Damage while there is at most one Rare or Unique Enemy nearby"]={{[1]={[1]={type="Condition",var="AtMostOneNearbyRareOrUniqueEnemy"},flags=0,keywordFlags=0,name="Damage",type="MORE",value=25}},nil} c["25% more Damage with Hits against Enemies that cannot have Life Leeched from them"]={{[1]={flags=0,keywordFlags=262144,name="Damage",type="MORE",value=25}}," against Enemies that cannot have Life Leeched from them "} c["25% more Maximum Life"]={{[1]={flags=0,keywordFlags=0,name="Life",type="MORE",value=25}},nil} -c["25% more Maximum Lightning Damage"]={{[1]={flags=0,keywordFlags=0,name="LightningDamage",type="MORE",value=25}}," Maximum "} -c["25% more Maximum Lightning Damage 50% less Minimum Lightning Damage"]={{[1]={flags=0,keywordFlags=0,name="LightningDamage",type="MORE",value=25}}," Maximum 50% less Minimum Lightning Damage "} -c["25% more Maximum Lightning Damage 50% less Minimum Lightning Damage Cannot deal non-Lightning Damage"]={{[1]={flags=0,keywordFlags=0,name="LightningDamage",type="MORE",value=25}}," Maximum 50% less Minimum Lightning Damage Cannot deal non-Lightning Damage "} +c["25% more Maximum Lightning Damage"]={{[1]={flags=0,keywordFlags=0,name="MaxLightningDamage",type="MORE",value=25}},nil} c["25% more Melee Critical Strike Chance while Blinded"]={{[1]={[1]={type="Condition",var="Blinded"},[2]={neg=true,type="Condition",var="CannotBeBlinded"},flags=256,keywordFlags=0,name="CritChance",type="MORE",value=25}},nil} c["25% more Melee Physical Damage during effect"]={{[1]={[1]={type="Condition",var="UsingFlask"},flags=256,keywordFlags=0,name="PhysicalDamage",type="MORE",value=25}},nil} c["25% more Physical and Chaos Damage Taken while Sane"]={{[1]={[1]={neg=true,type="Condition",var="Insane"},flags=0,keywordFlags=0,name="PhysicalDamageTaken",type="MORE",value=25},[2]={[1]={neg=true,type="Condition",var="Insane"},flags=0,keywordFlags=0,name="ChaosDamageTaken",type="MORE",value=25}},nil} @@ -5832,8 +5830,7 @@ c["50% less Impale Duration"]={{[1]={flags=0,keywordFlags=0,name="ImpaleDuration c["50% less Life Recovery from Flasks"]={{[1]={flags=0,keywordFlags=0,name="FlaskLifeRecovery",type="MORE",value=-50}},nil} c["50% less Life Regeneration Rate"]={{[1]={flags=0,keywordFlags=0,name="LifeRegen",type="MORE",value=-50}},nil} c["50% less Lightning Resistance"]={{[1]={flags=0,keywordFlags=0,name="LightningResist",type="MORE",value=-50}},nil} -c["50% less Minimum Lightning Damage"]={{[1]={flags=0,keywordFlags=0,name="LightningDamage",type="MORE",value=-50}}," Minimum "} -c["50% less Minimum Lightning Damage Cannot deal non-Lightning Damage"]={{[1]={flags=0,keywordFlags=0,name="LightningDamage",type="MORE",value=-50}}," Minimum Cannot deal non-Lightning Damage "} +c["50% less Minimum Lightning Damage"]={{[1]={flags=0,keywordFlags=0,name="MinLightningDamage",type="MORE",value=-50}},nil} c["50% less Poison Duration"]={{[1]={flags=0,keywordFlags=0,name="EnemyPoisonDuration",type="MORE",value=-50}},nil} c["50% less maximum Total Life Recovery per Second from Leech"]={{[1]={flags=0,keywordFlags=0,name="MaxLifeLeechRate",type="MORE",value=-50}},nil} c["50% more Accuracy Rating against Marked Enemy"]={{[1]={[1]={actor="enemy",type="ActorCondition",var="Marked"},flags=0,keywordFlags=0,name="AccuracyVsEnemy",type="MORE",value=50}},nil} @@ -7734,9 +7731,7 @@ c["Banner Skills have 8% increased Duration"]={{[1]={[1]={skillType=99,type="Ski c["Banner Skills have no Reservation"]={{[1]={[1]={skillType=99,type="SkillType"},[2]={neg=true,skillType=119,type="SkillType"},flags=0,keywordFlags=0,name="SkillData",type="LIST",value={key="manaReservationPercent",value=0}},[2]={[1]={skillType=99,type="SkillType"},[2]={neg=true,skillType=119,type="SkillType"},flags=0,keywordFlags=0,name="SkillData",type="LIST",value={key="lifeReservationPercent",value=0}}},nil} c["Banners also grant +5% to all Elemental Resistances to you and Allies"]={{[1]={[1]={skillType=99,type="SkillType"},flags=0,keywordFlags=0,name="ExtraAuraEffect",type="LIST",value={mod={flags=0,keywordFlags=0,name="ElementalResist",type="BASE",value=5}}}},nil} c["Base Critical Strike Chance for Attacks with Weapons is 8%"]={{[1]={flags=0,keywordFlags=0,name="WeaponBaseCritChance",type="OVERRIDE",value=8}},nil} -c["Base Ignite Duration is 1 second"]={{[1]={flags=0,keywordFlags=0,name="EnemyIgniteDuration",type="OVERRIDE",value=1}},"Base second "} -c["Base Ignite Duration is 1 second 25% less Damage with Ignite"]={{[1]={flags=0,keywordFlags=0,name="EnemyIgniteDuration",type="OVERRIDE",value=1}},"Base second 25% less Damage with Ignite "} -c["Base Ignite Duration is 1 second 25% less Damage with Ignite Cannot deal non-Fire Damage"]={{[1]={flags=0,keywordFlags=0,name="EnemyIgniteDuration",type="OVERRIDE",value=1}},"Base second 25% less Damage with Ignite Cannot deal non-Fire Damage "} +c["Base Ignite Duration is 1 second"]={{[1]={flags=0,keywordFlags=0,name="IgniteDurationBase",type="OVERRIDE",value=1}},nil} c["Base Spell Critical Strike Chance of Spells is equal to that of Main Hand Weapon"]={{[1]={flags=2,keywordFlags=0,name="BaseCritFromMainHand",type="FLAG",value=true}},nil} c["Bathed in the blood of 4050 sacrificed in the name of Ahuana"]={{[1]={flags=0,keywordFlags=0,name="JewelData",type="LIST",value={key="conqueredBy",value={conqueror={id="2_v2",type="vaal"},id=4050}}}},nil} c["Bathed in the blood of 8000 sacrificed in the name of Ahuana"]={{[1]={flags=0,keywordFlags=0,name="JewelData",type="LIST",value={key="conqueredBy",value={conqueror={id="2_v2",type="vaal"},id=8000}}}},nil} @@ -7922,10 +7917,10 @@ c["Cannot be Stunned while you have at least 25 Rage"]={{[1]={[1]={threshold=25, c["Cannot be used with Chaos Inoculation"]={nil,"Cannot be used with Chaos Inoculation "} c["Cannot be used with Chaos Inoculation Reserves 30% of Life"]={nil,"Cannot be used with Chaos Inoculation Reserves 30% of Life "} c["Cannot deal Critical Strikes with Attacks"]={{[1]={flags=1,keywordFlags=0,name="NeverCrit",type="FLAG",value=true},[2]={flags=1,keywordFlags=0,name="Condition:NeverCrit",type="FLAG",value=true}},nil} -c["Cannot deal non-Chaos Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true}},nil} -c["Cannot deal non-Cold Damage"]={nil,"Cannot deal non-Cold Damage "} -c["Cannot deal non-Fire Damage"]={nil,"Cannot deal non-Fire Damage "} -c["Cannot deal non-Lightning Damage"]={nil,"Cannot deal non-Lightning Damage "} +c["Cannot deal non-Chaos Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true}},nil} +c["Cannot deal non-Cold Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}},nil} +c["Cannot deal non-Fire Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}},nil} +c["Cannot deal non-Lightning Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}},nil} c["Cannot gain Energy Shield"]={{[1]={flags=0,keywordFlags=0,name="CannotGainEnergyShield",type="FLAG",value=true}},nil} c["Cannot gain Life during effect"]={{[1]={[1]={type="Condition",var="UsingFlask"},flags=0,keywordFlags=0,name="CannotGainLife",type="FLAG",value=true}},nil} c["Cannot gain Mana during effect"]={{[1]={[1]={type="Condition",var="UsingFlask"},flags=0,keywordFlags=0,name="CannotGainMana",type="FLAG",value=true}},nil} @@ -8272,9 +8267,9 @@ c["Deal no Cold Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoCold",type="F c["Deal no Damage when not on Low Life"]={{[1]={[1]={neg=true,type="Condition",var="LowLife"},flags=0,keywordFlags=0,name="DealNoDamage",type="FLAG",value=true}},nil} c["Deal no Elemental Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true}},nil} c["Deal no Non-Elemental Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}},nil} -c["Deal no Non-Fire Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}},nil} +c["Deal no Non-Fire Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}},nil} c["Deal no Non-Lightning Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}},nil} -c["Deal no Non-Physical Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}},nil} +c["Deal no Non-Physical Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}},nil} c["Deal no Physical Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true}},nil} c["Deal no Physical or Elemental Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true}},nil} c["Deal up to 15% more Melee Damage to Enemies, based on proximity"]={{[1]={[1]={ramp={[1]=1,[2]=0},type="MeleeProximity"},flags=257,keywordFlags=0,name="Damage",type="MORE",value=15}},nil} @@ -8392,9 +8387,7 @@ c["Enemies Cannot Leech Mana From you"]={nil,"Enemies Cannot Leech Mana From you c["Enemies Cannot Leech Mana From you 10% of Damage taken Recouped as Mana"]={nil,"Enemies Cannot Leech Mana From you 10% of Damage taken Recouped as Mana "} c["Enemies Chilled by your Hits can be Shattered as though Frozen"]={nil,"your Hits can be Shattered as though Frozen "} c["Enemies Chilled by your Hits can be Shattered as though Frozen Enemies Chilled by your Hits lessen their Damage dealt by half of Chill Effect"]={nil,"your Hits can be Shattered as though Frozen Enemies Chilled by your Hits lessen their Damage dealt by half of Chill Effect "} -c["Enemies Chilled by your Hits have Cold Damage taken increased by Chill Effect"]={nil,"your Hits have Cold Damage taken increased by Chill Effect "} -c["Enemies Chilled by your Hits have Cold Damage taken increased by Chill Effect Enemies in your Chilling Areas have Cold Damage taken increased by Chill Effect"]={nil,"your Hits have Cold Damage taken increased by Chill Effect Enemies in your Chilling Areas have Cold Damage taken increased by Chill Effect "} -c["Enemies Chilled by your Hits have Cold Damage taken increased by Chill Effect Enemies in your Chilling Areas have Cold Damage taken increased by Chill Effect Cannot deal non-Cold Damage"]={nil,"your Hits have Cold Damage taken increased by Chill Effect Enemies in your Chilling Areas have Cold Damage taken increased by Chill Effect Cannot deal non-Cold Damage "} +c["Enemies Chilled by your Hits have Cold Damage taken increased by Chill Effect"]={{[1]={flags=0,keywordFlags=0,name="ChillEffectIncColdDamageTaken",type="FLAG",value=true}},nil} c["Enemies Chilled by your Hits have Damage taken increased by Chill Effect"]={{[1]={flags=0,keywordFlags=0,name="ChillEffectIncDamageTaken",type="FLAG",value=true}},nil} c["Enemies Chilled by your Hits lessen their Damage dealt by half of Chill Effect"]={{[1]={flags=0,keywordFlags=0,name="ChillEffectLessDamageDealt",type="FLAG",value=true}},nil} c["Enemies Cursed by you are Hindered if 25% of Curse Duration expired"]={{[1]={flags=0,keywordFlags=0,name="EnemyModifier",type="LIST",value={mod={[1]={threshold=25,type="MultiplierThreshold",var="CurseExpired"},[2]={type="ActorCondition",var="Cursed"},flags=0,keywordFlags=0,name="Condition:Hindered",type="FLAG",value=true}}}},nil} @@ -8447,8 +8440,7 @@ c["Enemies affected by your Spider's Webs deal 10% reduced Damage"]={{[1]={flags c["Enemies affected by your Spider's Webs have -10% to All Resistances"]={{[1]={flags=0,keywordFlags=0,name="EnemyModifier",type="LIST",value={mod={[1]={threshold=1,type="MultiplierThreshold",var="Spider's WebStack"},flags=0,keywordFlags=0,name="ElementalResist",type="BASE",value=-10}}},[2]={flags=0,keywordFlags=0,name="EnemyModifier",type="LIST",value={mod={[1]={threshold=1,type="MultiplierThreshold",var="Spider's WebStack"},flags=0,keywordFlags=0,name="ChaosResist",type="BASE",value=-10}}}},nil} c["Enemies display their Monster Category"]={nil,"Enemies display their Monster Category "} c["Enemies display their Monster Category 120% increased Evasion and Energy Shield"]={nil,"Enemies display their Monster Category 120% increased Evasion and Energy Shield "} -c["Enemies in your Chilling Areas have Cold Damage taken increased by Chill Effect"]={nil,"Enemies in your Chilling Areas have Cold Damage taken increased by Chill Effect "} -c["Enemies in your Chilling Areas have Cold Damage taken increased by Chill Effect Cannot deal non-Cold Damage"]={nil,"Enemies in your Chilling Areas have Cold Damage taken increased by Chill Effect Cannot deal non-Cold Damage "} +c["Enemies in your Chilling Areas have Cold Damage taken increased by Chill Effect"]={{[1]={[1]={actor="enemy",type="ActorCondition",var="InChillingArea"},flags=0,keywordFlags=0,name="ChillingAreaIncColdDamageTaken",type="FLAG",value=true}},nil} c["Enemies in your Chilling Areas take 30% increased Lightning Damage"]={{[1]={[1]={actor="enemy",type="ActorCondition",var="InChillingArea"},flags=0,keywordFlags=0,name="EnemyModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="LightningDamageTaken",type="INC",value=30}}}},nil} c["Enemies in your Chilling Areas take 35% increased Lightning Damage"]={{[1]={[1]={actor="enemy",type="ActorCondition",var="InChillingArea"},flags=0,keywordFlags=0,name="EnemyModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="LightningDamageTaken",type="INC",value=35}}}},nil} c["Enemies in your Link Beams cannot apply Elemental Ailments"]={{[1]={[1]={actor="enemy",type="ActorCondition",var="BetweenYouAndLinkedTarget"},flags=0,keywordFlags=0,name="ElementalAilmentImmune",type="FLAG",value=true}},nil} @@ -9968,7 +9960,7 @@ c["Minions deal 80% increased Damage"]={{[1]={flags=0,keywordFlags=0,name="Minio c["Minions deal 80% increased Damage if you have Warcried Recently"]={{[1]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={[1]={actor="parent",type="ActorCondition",var="UsedWarcryRecently"},flags=0,keywordFlags=0,name="Damage",type="INC",value=80}}}},nil} c["Minions deal 9 to 15 additional Cold Damage"]={{[1]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="ColdMin",type="BASE",value=9}}},[2]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="ColdMax",type="BASE",value=15}}}},nil} c["Minions deal 96 to 144 additional Physical Damage"]={{[1]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="PhysicalMin",type="BASE",value=96}}},[2]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="PhysicalMax",type="BASE",value=144}}}},nil} -c["Minions deal no Non-Cold Damage"]={{[1]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true}}},[2]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true}}},[3]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true}}},[4]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}}}},nil} +c["Minions deal no Non-Cold Damage"]={{[1]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true}}},[2]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true}}},[3]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true}}},[4]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}}}},nil} c["Minions from Herald Skills deal 25% more Damage"]={{[1]={[1]={skillType=62,type="SkillType"},flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="Damage",type="MORE",value=25}}}},nil} c["Minions gain 18% of Elemental Damage as Extra Chaos Damage"]={{[1]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="ElementalDamageGainAsChaos",type="BASE",value=18}}}},nil} c["Minions gain 20% of Elemental Damage as Extra Chaos Damage"]={{[1]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="ElementalDamageGainAsChaos",type="BASE",value=20}}}},nil} diff --git a/src/Modules/ModParser.lua b/src/Modules/ModParser.lua index 94957513b8..0b1ef1bf01 100644 --- a/src/Modules/ModParser.lua +++ b/src/Modules/ModParser.lua @@ -153,6 +153,7 @@ local formList = { } -- Map of modifier names +local damageTypeList = { "Physical", "Lightning", "Cold", "Fire", "Chaos" } local modNameList = { -- Attributes ["strength"] = "Str", @@ -690,7 +691,7 @@ local modNameList = { ["physical attack damage"] = { "PhysicalDamage", flags = ModFlag.Attack }, ["minimum physical attack damage"] = { "MinPhysicalDamage", tag = { type = "SkillType", skillType = SkillType.Attack } }, ["maximum physical attack damage"] = { "MaxPhysicalDamage", tag = { type = "SkillType", skillType = SkillType.Attack } }, - ["maximum attack damage"] = { "MinDamage", tag = { type = "SkillType", skillType = SkillType.Attack } }, + ["minimum attack damage"] = { "MinDamage", tag = { type = "SkillType", skillType = SkillType.Attack } }, ["maximum attack damage"] = { "MaxDamage", tag = { type = "SkillType", skillType = SkillType.Attack } }, ["physical weapon damage"] = { "PhysicalDamage", flags = ModFlag.Weapon }, ["physical damage with weapons"] = { "PhysicalDamage", flags = ModFlag.Weapon }, @@ -888,6 +889,11 @@ local modNameList = { ["resistance shrine buff"] = "Condition:ResistanceShrine", ["resonating shrine buff"] = "Condition:ResonatingShrine", } +for _, damageType in ipairs(damageTypeList) do + local lowerDamageType = damageType:lower() + modNameList["minimum "..lowerDamageType.." damage"] = "Min"..damageType.."Damage" + modNameList["maximum "..lowerDamageType.." damage"] = "Max"..damageType.."Damage" +end -- List of modifier flags local modFlagList = { @@ -1977,24 +1983,20 @@ local function flag(name, ...) return mod(name, "FLAG", true, ...) end --- Returns the DealNo flags for every damage type except the given one, for the universal --- "deal no non- damage" / "cannot deal non- damage" handlers. --- "elemental" is not a single type and keeps its own literal entry; unknown types return nil --- so the line falls through as unsupported instead of wrongly disabling all damage. -local dealNoNonDamageTypeList = { "Physical", "Cold", "Fire", "Lightning", "Chaos" } -local function dealNoNonDamageType(dmgType) - local keep = firstToUpper(dmgType) - local flags = { } - for _, t in ipairs(dealNoNonDamageTypeList) do - if t ~= keep then - t_insert(flags, flag("DealNo"..t)) - end +-- Makes the "deal no" modifiers for every damage type except the one being kept. +local function dealNoNonDamageType(dmgType, forMinion) + dmgType = firstToUpper(dmgType) + if not isValueInArray(damageTypeList, dmgType) then + return end - if #flags == #dealNoNonDamageTypeList then - -- dmgType didn't match any known damage type - return nil + local mods = { } + for _, damageType in ipairs(damageTypeList) do + if damageType ~= dmgType then + local dealNo = flag("DealNo"..damageType) + t_insert(mods, forMinion and mod("MinionModifier", "LIST", { mod = dealNo }) or dealNo) + end end - return flags + return mods end local gemIdLookup = { @@ -4337,17 +4339,7 @@ local specialModList = { mod("MinionModifier", "LIST", { mod = flag("Condition:DiamondShrine") }, { type = "SkillName", skillName = "Summon Phantasm" }), mod("MinionModifier", "LIST", { mod = flag("Condition:MassiveShrine") }, { type = "SkillName", skillName = "Summon Phantasm" }), }, - ["minions deal no non%-(%a+) damage"] = function(_, dmgType) - local flags = dealNoNonDamageType(dmgType) - if not flags then - return nil - end - local mods = { } - for _, dealNoFlag in ipairs(flags) do - t_insert(mods, mod("MinionModifier", "LIST", { mod = dealNoFlag })) - end - return mods - end, + ["minions deal no non%-(%a+) damage"] = function(_, dmgType) return dealNoNonDamageType(dmgType, true) end, ["minions convert (%d+)%% of (.+) damage to (.+) damage"] = function(num, _, source, target) return { mod("MinionModifier", "LIST", { mod = mod(source:gsub("^%l", string.upper) .. "DamageConvertTo" .. target:gsub("^%l", string.upper), "BASE", num) }) } end, @@ -5284,23 +5276,6 @@ local specialModList = { ["attacks with this weapon have added maximum lightning damage equal to (%d+)%% of player'?s? maximum energy shield"] = function(num) return { mod("LightningMax", "BASE", 1, { type = "PercentStat", stat = "EnergyShield" , percent = num, actor = "parent" }, { type = "Condition", var = "{Hand}Attack" }, { type = "SkillType", skillType = SkillType.Attack }), } end, - -- cspell:ignore imum - -- Scaling the minimum/maximum roll of a damage type (covers all types, e.g. "maximum lightning damage", "minimum cold damage"). - -- `(m[ia][xn]imum)` captures the literal words "maximum" or "minimum". These map to the MinDamage/MaxDamage stats - -- consumed by calcDamage in CalcOffence. "more"/"less" apply as MORE: - ["(%d+)%% more (m[ia][xn]imum) (%a+) damage"] = function(num, _, minMax, dmgType) return { - mod((minMax == "maximum" and "Max" or "Min")..firstToUpper(dmgType).."Damage", "MORE", num), - } end, - ["(%d+)%% less (m[ia][xn]imum) (%a+) damage"] = function(num, _, minMax, dmgType) return { - mod((minMax == "maximum" and "Max" or "Min")..firstToUpper(dmgType).."Damage", "MORE", -num), - } end, - -- ...while "increased"/"reduced" apply as INC: - ["(%d+)%% increased (m[ia][xn]imum) (%a+) damage"] = function(num, _, minMax, dmgType) return { - mod((minMax == "maximum" and "Max" or "Min")..firstToUpper(dmgType).."Damage", "INC", num), - } end, - ["(%d+)%% reduced (m[ia][xn]imum) (%a+) damage"] = function(num, _, minMax, dmgType) return { - mod((minMax == "maximum" and "Max" or "Min")..firstToUpper(dmgType).."Damage", "INC", -num), - } end, ["adds (%d+)%% of your maximum mana as fire damage to attacks with this weapon"] = function(num) return { mod("FireMin", "BASE", 1, { type = "PercentStat", stat = "Mana" , percent = num }, { type = "Condition", var = "{Hand}Attack" }, { type = "SkillType", skillType = SkillType.Attack }), mod("FireMax", "BASE", 1, { type = "PercentStat", stat = "Mana" , percent = num }, { type = "Condition", var = "{Hand}Attack" }, { type = "SkillType", skillType = SkillType.Attack }), From 8c4f586e87152f09d4756c0bbf0ed157d5029e03 Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Fri, 24 Jul 2026 07:31:31 +1000 Subject: [PATCH 9/9] Change ordering --- src/Modules/CalcPerform.lua | 2 -- src/Modules/ModParser.lua | 17 +++++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Modules/CalcPerform.lua b/src/Modules/CalcPerform.lua index 5d14944ea1..67ad0faed2 100644 --- a/src/Modules/CalcPerform.lua +++ b/src/Modules/CalcPerform.lua @@ -3411,8 +3411,6 @@ function calcs.perform(env, skipEHP) t_insert(mods, modLib.createMod("DamageTaken", "INC", num, "Ahuana's Bite", { type = "Condition", var = "Chilled" })) elseif modDB:Flag(nil, "ChillEffectIncColdDamageTaken") then t_insert(mods, modLib.createMod("ColdDamageTaken", "INC", num, "Chilled by Hits", { type = "Condition", var = "Chilled" })) - -- Scales off the current chill effect on the enemy; the flag itself is gated on the - -- enemy being in a chilling area (config option) elseif modDB:Flag(nil, "ChillingAreaIncColdDamageTaken") then t_insert(mods, modLib.createMod("ColdDamageTaken", "INC", num, "Chilling Area", { type = "Condition", var = "Chilled" })) elseif output.HasBonechill and (hasGuaranteedBonechill or enemyDB:Sum("BASE", nil, "ChillVal") > 0) then diff --git a/src/Modules/ModParser.lua b/src/Modules/ModParser.lua index 0b1ef1bf01..889f7d3ba2 100644 --- a/src/Modules/ModParser.lua +++ b/src/Modules/ModParser.lua @@ -153,7 +153,6 @@ local formList = { } -- Map of modifier names -local damageTypeList = { "Physical", "Lightning", "Cold", "Fire", "Chaos" } local modNameList = { -- Attributes ["strength"] = "Str", @@ -679,10 +678,20 @@ local modNameList = { -- Basic damage types ["damage"] = "Damage", ["physical damage"] = "PhysicalDamage", + ["minimum physical damage"] = "MinPhysicalDamage", + ["maximum physical damage"] = "MaxPhysicalDamage", ["lightning damage"] = "LightningDamage", + ["minimum lightning damage"] = "MinLightningDamage", + ["maximum lightning damage"] = "MaxLightningDamage", ["cold damage"] = "ColdDamage", + ["minimum cold damage"] = "MinColdDamage", + ["maximum cold damage"] = "MaxColdDamage", ["fire damage"] = "FireDamage", + ["minimum fire damage"] = "MinFireDamage", + ["maximum fire damage"] = "MaxFireDamage", ["chaos damage"] = "ChaosDamage", + ["minimum chaos damage"] = "MinChaosDamage", + ["maximum chaos damage"] = "MaxChaosDamage", ["non-chaos damage"] = "NonChaosDamage", ["elemental damage"] = "ElementalDamage", -- Other damage forms @@ -889,11 +898,6 @@ local modNameList = { ["resistance shrine buff"] = "Condition:ResistanceShrine", ["resonating shrine buff"] = "Condition:ResonatingShrine", } -for _, damageType in ipairs(damageTypeList) do - local lowerDamageType = damageType:lower() - modNameList["minimum "..lowerDamageType.." damage"] = "Min"..damageType.."Damage" - modNameList["maximum "..lowerDamageType.." damage"] = "Max"..damageType.."Damage" -end -- List of modifier flags local modFlagList = { @@ -1982,6 +1986,7 @@ local mod = modLib.createMod local function flag(name, ...) return mod(name, "FLAG", true, ...) end +local damageTypeList = { "Physical", "Lightning", "Cold", "Fire", "Chaos" } -- Makes the "deal no" modifiers for every damage type except the one being kept. local function dealNoNonDamageType(dmgType, forMinion)