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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions spec/System/TestDefence_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ describe("TestDefence", function()
-- newBuild() takes care of resetting everything in setup()
end)

it("converts physical damage over time independently of hit damage", function()
build.configTab.input.enemyDamageType = "DamageOverTime"
for _, damageType in ipairs({ "Physical", "Lightning", "Cold", "Fire", "Chaos" }) do
build.configTab.input["enemy"..damageType.."Damage"] = damageType == "Physical" and 1000 or 0
end
local common = "+300% to Chaos Resistance\n10% of Physical Damage taken as Chaos Damage\n20% of Physical Damage from Hits taken as Chaos Damage"
-- Shared, hit-only and DoT-only conversion must stay separate, including on removal.
for _, value in ipairs({ 10, 0 }) do
build.configTab.input.customMods = common..(value > 0 and "\n"..value.."% of Physical Damage over Time taken as Chaos Damage" or "")
build.configTab:BuildModList()
runCallback("OnFrame")
local actor = build.calcsTab.mainEnv.player
local output = actor.output
assert.are.equal(925 - 7.5 * value, output.TotalDegen)
assert.are.equal(70, actor.damageShiftTable.Physical.Physical)
assert.are.equal(30, actor.damageShiftTable.Physical.Chaos)
assert.are.equal(100, actor.damageOverTimeShiftTable.Cold.Cold)
end
end)

-- a small helper function to calculate damage taken from limited test parameters
local function takenHitFromTypeMaxHit(type, enemyDamageMulti)
return build.calcsTab.calcs.takenHitFromDamage(build.calcsTab.calcsOutput[type.."MaximumHitTaken"] * (enemyDamageMulti or 1), type, build.calcsTab.calcsEnv.player)
Expand Down
346 changes: 346 additions & 0 deletions spec/System/TestItemMods_spec.lua

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src/Classes/ConfigTab.lua
Original file line number Diff line number Diff line change
Expand Up @@ -968,7 +968,8 @@ function ConfigTabClass:Save(xml)
t_insert(xml, child)

for k, v in pairsSortByKey(configSet.input) do
if v ~= self:GetDefaultState(k, type(v)) then
-- This override distinguishes an explicit zero from the automatic missing-life assumption.
if k == "missingUnreservedLifePercent" or v ~= self:GetDefaultState(k, type(v)) then
local node = { elem = "Input", attrib = { name = k } }
if type(v) == "number" then
node.attrib.number = tostring(v)
Expand Down
2 changes: 2 additions & 0 deletions src/Classes/Item.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2649,6 +2649,8 @@ function ItemClass:BuildModList()
source = self.modSource,
triggered = skill.triggered,
triggerChance = skill.triggerChance,
-- Keep the influence requirement so calculation setup can enable or remove the granted curse.
itemRequirement = skill.itemRequirement,
})
end
end
Expand Down
6 changes: 5 additions & 1 deletion src/Classes/SkillsTab.lua
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ function SkillsTabClass:SkillsTab(build)
end
self.controls.sourceNote = new("LabelControl"):LabelControl({ "TOPLEFT", self.controls.socketsLabel, "TOPLEFT" }, { 0, 30, 0, 16 })
self.controls.sourceNote.shown = function()
return self.displayGroup.source ~= nil
return self.displayGroup.source ~= nil and self.displayGroup.source ~= "Config"
end
self.controls.sourceNote.label = function()
local label
Expand Down Expand Up @@ -1321,6 +1321,10 @@ function SkillsTabClass:AddSocketGroupTooltip(tooltip, socketGroup)
if sourceSingle then
tooltip:AddLine(18, "^7Source: " .. colorCodes[sourceSingle.rarity or "NORMAL"] .. sourceSingle.name)
tooltip:AddSeparator(10)
-- Identify curses granted by Custom Modifiers in the skill-group tooltip.
elseif socketGroup.source == "Config" then
tooltip:AddLine(18, "^7Source: " .. colorCodes.NORMAL .. "Custom Modifiers")
tooltip:AddSeparator(10)
end
local gemShown = { }
for index, activeSkill in ipairs(socketGroup.displaySkillList) do
Expand Down
74 changes: 37 additions & 37 deletions src/Data/ModCache.lua

Large diffs are not rendered by default.

28 changes: 26 additions & 2 deletions src/Modules/CalcDefence.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
---@class Calcs
local calcs = require("Modules.CalcBase")

local regenerationConversions = { { "LightRadius", "LifeRegen", "LightRadiusAppliesToLifeRegen" }, { "ManaRegen", "EnergyShieldRecharge", "ManaRegenAppliesToEnergyShieldRecharge" } }

local pairs = pairs
local ipairs = ipairs
local t_insert = table.insert
Expand Down Expand Up @@ -791,6 +793,18 @@ function calcs.defence(env, actor)
output.DamageTakenOnBlock = 100 - output.BlockEffect
end

-- Apply Light Radius increases to Life Regeneration, and Mana Regeneration increases to ES Recharge, at the stated percentage.
for _, conversion in ipairs(regenerationConversions) do
local multiplier = (modDB:Max(nil, conversion[3]) or 0) / 100
if multiplier ~= 0 then
for _, value in ipairs(modDB:Tabulate("INC", nil, conversion[1])) do
local mod = value.mod
local modifiers = calcLib.getConvertedModTags(mod, multiplier)
modDB:NewMod(conversion[2], "INC", m_floor(mod.value * multiplier), mod.source, mod.flags, mod.keywordFlags, unpack(modifiers))
end
end
end

if modDB:Flag(nil, "ArmourAppliesToEnergyShieldRecharge") then
-- Armour to ES Recharge conversion from Armour and Energy Shield Mastery
local multiplier = (modDB:Max(nil, "ImprovedArmourAppliesToEnergyShieldRecharge") or 100) / 100
Expand Down Expand Up @@ -1210,6 +1224,14 @@ function calcs.defence(env, actor)
}
end

-- Use the non-flask Petrified Blood recovery ceiling only as a default for this modifier.
local missingLife = 0
if modDB:GetCondition("AffectedByPetrifiedBlood") and output.LifeUnreserved > 0 then
local currentLife = m_min(output.LifeUnreserved, output.Life * output.LowLifePercentage / 100)
missingLife = 100 * (output.LifeUnreserved - currentLife) / output.LifeUnreserved
end
output.MissingUnreservedLifePercent = modDB:Override(nil, "MissingUnreservedLifePercent") or missingLife

-- Recovery modifiers
output.LifeRecoveryRateMod = 1
if not modDB:Flag(nil, "CannotRecoverLifeOutsideLeech") then
Expand Down Expand Up @@ -1857,9 +1879,11 @@ function calcs.buildDefenceEstimations(env, actor)
local dotDestinationTotal = 0
for _, destType in ipairs(dmgTypeList) do
if destType ~= damageType then
dotShiftTable[destType] = modDB:Sum("BASE", nil, damageType.."DamageTakenAs"..destType, isElemental[damageType] and "ElementalDamageTakenAs"..destType or nil)
-- Allow Physical DoT taken as Chaos without also converting damage from hits.
local takenAs = modDB:Sum("BASE", nil, damageType.."DamageTakenAs"..destType, isElemental[damageType] and "ElementalDamageTakenAs"..destType or nil)
dotShiftTable[destType] = takenAs + modDB:Sum("BASE", nil, damageType.."DamageTakenOverTimeAs"..destType, isElemental[damageType] and "ElementalDamageTakenOverTimeAs"..destType or nil)
dotDestinationTotal = dotDestinationTotal + dotShiftTable[destType]
shiftTable[destType] = dotShiftTable[destType] + modDB:Sum("BASE", nil, damageType.."DamageFromHitsTakenAs"..destType, isElemental[damageType] and "ElementalDamageFromHitsTakenAs"..destType or nil)
shiftTable[destType] = takenAs + modDB:Sum("BASE", nil, damageType.."DamageFromHitsTakenAs"..destType, isElemental[damageType] and "ElementalDamageFromHitsTakenAs"..destType or nil)
destTotal = destTotal + shiftTable[destType]
end
end
Expand Down
6 changes: 5 additions & 1 deletion src/Modules/CalcMirages.lua
Original file line number Diff line number Diff line change
Expand Up @@ -288,23 +288,27 @@ function calcs.mirages(env)
preCalcFunc = function(env, newSkill, newEnv)
local lessDamage = newSkill.skillModList:Sum("BASE", env.player.mainSkill.skillCfg, "SacredWispsLessDamage")
local wispsMaxCount
local wispsSummoned
local wispsCastChance

-- Find Wisps summoning skill for cast chance and wisp count
for _, skill in ipairs(env.player.activeSkillList) do
if skill.activeEffect.grantedEffect.name == "Summon Sacred Wisps" then
wispsCastChance = skill.skillModList:Sum("BASE", env.player.mainSkill.skillCfg, "SacredWispsChance")
wispsMaxCount = skill.skillModList:Sum("BASE", env.player.mainSkill.skillCfg, "SacredWispsMaxCount")
wispsSummoned = 2 + skill.skillModList:Sum("BASE", skill.skillCfg, "SacredWispsSummoned")
break
end
end

env.player.mainSkill.mirage = { }
env.player.mainSkill.mirage.name = newSkill.activeEffect.grantedEffect.name
-- Sustained damage uses the active cap; summons per trigger only affect ramp-up.
env.player.mainSkill.mirage.count = wispsMaxCount
env.player.mainSkill.mirage.summoned = wispsSummoned

if not env.player.mainSkill.infoMessage then
env.player.mainSkill.infoMessage = tostring(wispsMaxCount) .. " Sacred Wisps using " .. newSkill.activeEffect.grantedEffect.name
env.player.mainSkill.infoMessage = tostring(wispsMaxCount) .. " Sacred Wisps using " .. newSkill.activeEffect.grantedEffect.name .. " (" .. wispsSummoned .. " summoned per trigger)"
end

-- Add new modifiers to new skill (which already has all the old skill's modifiers)
Expand Down
8 changes: 5 additions & 3 deletions src/Modules/CalcOffence.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3593,7 +3593,8 @@ function calcs.offence(env, actor, activeSkill)
pen = skillModList:Sum("BASE", cfg, "ChaosPenetration")
end
end
local invertChanceEle = m_max(m_min(skillModList:Sum("CHANCE", cfg, "HitsInvertEleResChance"), 1), 0)
-- Include the matching element-specific resistance inversion chance when calculating hit damage.
local invertChanceEle = m_max(m_min(skillModList:Sum("CHANCE", cfg, "HitsInvertEleResChance", "HitsInvert"..damageType.."ResChance"), 1), 0)
local invertChanceChaos = m_max(m_min(skillModList:Sum("CHANCE", cfg, "HitsInvertChaosResChance"), 1), 0)
local invertChance = (isElemental[damageType] and invertChanceEle) or (damageType == "Chaos" and invertChanceChaos) or 0
sourceRes = env.modDB:Flag(nil, "Enemy"..sourceRes.."ResistEqualToYours") and "Your "..sourceRes.." Resistance" or (env.partyMembers.modDB:Flag(nil, "Enemy"..sourceRes.."ResistEqualToYours") and "Party Member "..sourceRes.." Resistance" or sourceRes)
Expand Down Expand Up @@ -5413,11 +5414,12 @@ function calcs.offence(env, actor, activeSkill)
local incChillDuration = skillModList:Sum("INC", cfg, "EnemyChillDuration", "EnemyAilmentDuration", "EnemyElementalAilmentDuration") + enemyDB:Sum("INC", nil, "SelfChillDuration", "SelfAilmentDuration", "SelfElementalAilmentDuration")
local moreChillDuration = skillModList:More(cfg, "EnemyChillDuration", "EnemyAilmentDuration", "EnemyElementalAilmentDuration") * enemyDB:More(nil, "SelfChillDuration", "SelfAilmentDuration", "SelfElementalAilmentDuration")
output.ChillDurationMod = (1 + incChillDuration / 100) * moreChillDuration
output.ChillSourceEffect = m_min(skillModList:Override(nil, "ChillMax") or ailmentData.Chill.max, m_floor(ailmentData.Chill.default * output.ChillEffectMod))
-- Let +maximum Chill effect also raise the cap for chilling areas such as Cold Snap.
output.ChillSourceEffect = m_min(skillModList:Override(nil, "ChillMax") or (ailmentData.Chill.max + skillModList:Sum("BASE", nil, "ChillMax")), m_floor(ailmentData.Chill.default * output.ChillEffectMod))
if breakdown then
breakdown.DotChill = { }
breakdown.multiChain(breakdown.DotChill, {
label = s_format("Effect of Chill: ^8(capped at %d%%)", skillModList:Override(nil, "ChillMax") or ailmentData.Chill.max),
label = s_format("Effect of Chill: ^8(capped at %d%%)", skillModList:Override(nil, "ChillMax") or (ailmentData.Chill.max + skillModList:Sum("BASE", nil, "ChillMax"))),
base = s_format("%d%% ^8(base)", ailmentData.Chill.default),
{ "%.2f ^8(increased/reduced effect of chill)", 1 + incChill / 100 },
{ "%.2f ^8(more/less effect of chill)", moreChill },
Expand Down
4 changes: 4 additions & 0 deletions src/Modules/CalcPerform.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1700,6 +1700,10 @@ function calcs.perform(env, skipEHP)

local function calcFlaskMods(item, baseName, buffModList, modList, onlyMinion)
local flaskEffectInc = effectInc + item.flaskData.effectInc
-- Apply increased Iron Flask effect to the player without boosting other flask types.
if item.baseName == "Iron Flask" then
flaskEffectInc = flaskEffectInc + modDB:Sum("INC", { actor = "player" }, "IronFlaskEffect")
end
local flaskEffectIncNonPlayer = effectIncNonPlayer + item.flaskData.effectInc
if item.rarity == "MAGIC" and not (item.base.flask.life or item.base.flask.mana) then
flaskEffectInc = flaskEffectInc + effectIncMagic
Expand Down
41 changes: 37 additions & 4 deletions src/Modules/CalcSetup.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,21 @@ function calcs.initEnv(build, mode, override, specEnv)
end
end

-- Wickerman slots follow ordinary equipment. Collect their bonus modifiers
-- before scaling gloves/boots so the result does not depend on slot order.
-- Glove/boot bonus scaling must include Custom Modifiers and Wickerman items as well as passive nodes.
local equipmentBonusModDB = new("ModDB"):ModDB(env.initialNodeModDB)
equipmentBonusModDB:AddList(build.configTab.modList)
for _, item in pairs(items) do
if item.type:match("^Wickerman ") then
for _, mod in ipairs(item.modList or { }) do
if mod.name == "EffectOfBonusesFromGloves" or mod.name == "EffectOfBonusesFromBoots" then
equipmentBonusModDB:AddMod(mod)
end
end
end
end

env.itemSlotIndex = { }
-- Track which flask slot (1-5) each flask is in, for adjacency checks
env.flaskSlotMap = { }
Expand Down Expand Up @@ -1257,8 +1272,9 @@ function calcs.initEnv(build, mode, override, specEnv)
combinedList:MergeMod(mod)
end
env.itemModDB:ScaleAddList(combinedList, scale)
elseif item.type == "Gloves" and calcLib.mod(env.initialNodeModDB, nil, "EffectOfBonusesFromGloves") ~=1 then
scale = calcLib.mod(env.initialNodeModDB, nil, "EffectOfBonusesFromGloves") - 1
-- Apply increased/reduced bonuses from equipped gloves from Custom Modifiers and Wickerman items, as well as passives.
elseif item.type == "Gloves" and calcLib.mod(equipmentBonusModDB, nil, "EffectOfBonusesFromGloves") ~=1 then
scale = calcLib.mod(equipmentBonusModDB, nil, "EffectOfBonusesFromGloves") - 1
local combinedList = new("ModList"):ModList()
for _, mod in ipairs(srcList) do
combinedList:MergeMod(mod)
Expand All @@ -1269,8 +1285,9 @@ function calcs.initEnv(build, mode, override, specEnv)
combinedList:MergeMod(mod, true)
end
env.itemModDB:AddList(combinedList)
elseif item.type == "Boots" and calcLib.mod(env.initialNodeModDB, nil, "EffectOfBonusesFromBoots") ~= 1 then
scale = calcLib.mod(env.initialNodeModDB, nil, "EffectOfBonusesFromBoots") - 1
-- Apply increased/reduced bonuses from equipped boots from Custom Modifiers and Wickerman items, as well as passives.
elseif item.type == "Boots" and calcLib.mod(equipmentBonusModDB, nil, "EffectOfBonusesFromBoots") ~= 1 then
scale = calcLib.mod(equipmentBonusModDB, nil, "EffectOfBonusesFromBoots") - 1
local combinedList = new("ModList"):ModList()
for _, mod in ipairs(srcList) do
combinedList:MergeMod(mod)
Expand Down Expand Up @@ -1463,6 +1480,22 @@ function calcs.initEnv(build, mode, override, specEnv)

-- Merge Granted Skills Tables
env.grantedSkills = tableConcat(env.grantedSkillsNodes, env.grantedSkillsItems)
-- Create skill groups for influence-gated curses entered in Custom Modifiers, just like item-granted curses.
for _, skill in ipairs(build.configTab.modList:List(nil, "ExtraSkill")) do
if skill.itemRequirement and env.data.skills[skill.skillId] then
local grantedSkill = copyTable(skill)
grantedSkill.source = "Config"
grantedSkill.nameSpec = env.data.skills[skill.skillId].name
t_insert(env.grantedSkills, grantedSkill)
end
end
-- Only grant these curses while enough items of the required influence are equipped.
for index = #env.grantedSkills, 1, -1 do
local requirement = env.grantedSkills[index].itemRequirement
if requirement and env.itemModDB:GetMultiplier(requirement.multiplier) < requirement.count then
t_remove(env.grantedSkills, index)
end
end

if not accelerate.skills then
if env.mode == "MAIN" then
Expand Down
5 changes: 5 additions & 0 deletions src/Modules/ConfigOptions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ return {
{ var = "conditionFullLife", type = "check", label = "Are you always on Full ^xE05030Life?", ifCond = "FullLife", tooltip = FullLifeTooltip, apply = function(val, modList, enemyModList)
modList:NewMod("Condition:FullLife", "FLAG", true, "Config")
end },
{ var = "missingUnreservedLifePercent", type = "countAllowZero", label = "Missing unreserved Life % (override):", ifStat = "MissingUnreservedLifePercent",
tooltip = "For recovery scaling per missing unreserved Life.\nBlank assumes full unreserved Life, or Petrified Blood's non-flask recovery limit:\n50% maximum Life (55% with the Low Life mastery), accounting for reservation.\nAn explicit value, including 0, overrides this assumption.\nDoes not change other Full/Low Life settings.",
apply = function(val, modList, enemyModList)
modList:NewMod("MissingUnreservedLifePercent", "OVERRIDE", math.max(0, math.min(100, val)), "Config")
end },
{ var = "conditionLowLife", type = "check", label = "Are you always on Low ^xE05030Life?", ifCond = "LowLife", tooltip = LowLifeTooltip, apply = function(val, modList, enemyModList)
modList:NewMod("Condition:LowLife", "FLAG", true, "Config")
end },
Expand Down
Loading
Loading