Skip to content

Commit

Permalink
Fix maximum sustainable stages calculations ignoring certain buffs/mo…
Browse files Browse the repository at this point in the history
…ds (#7678)

* FIX: maximum sustainable stacks calculations ignoring certain buffs/mods

Essentially an improved version of
#5164 that
utilizes skill cache functionality.

* FIX: spelling

* TEST: add tests for schorching ray and blight interactions

* FIX: formatting

* DOCS: add fancy annotation for new function

* FIX: calculate the correct skill

* FIX: annotation

* FIX: use activation frequency for penance brand
  • Loading branch information
Paliak authored Jul 21, 2024
1 parent b6a18ce commit e9dd799
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 137 deletions.
33 changes: 33 additions & 0 deletions spec/System/TestSkills_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,37 @@ describe("TestAttacks", function()

assert.True(build.calcsTab.mainOutput.MirageDPS ~= nil)
end)

it("Test Scorching ray applying exposure at max stages", function()
build.skillsTab:PasteSocketGroup("Scorching Ray 20/0 Default 1\n")
runCallback("OnFrame")

local mainSocketGroup = build.skillsTab.socketGroupList[build.mainSocketGroup]
local srcInstance = mainSocketGroup.displaySkillList[mainSocketGroup.mainActiveSkill].activeEffect.srcInstance
srcInstance.skillStageCount = 8
build.modFlag = true
build.buildFlag = true
runCallback("OnFrame")

assert.True(build.calcsTab.mainEnv.enemyDB:Sum("BASE", nil, "FireResist") < 0)
end)

it("Test Adrenaline affecting blight max stage count", function()
build.skillsTab:PasteSocketGroup("Blight 20/0 Default 1\n")
runCallback("OnFrame")

local mainSocketGroup = build.skillsTab.socketGroupList[build.mainSocketGroup]
local srcInstance = mainSocketGroup.displaySkillList[mainSocketGroup.mainActiveSkill].activeEffect.srcInstance
srcInstance.skillPart = 2
build.modFlag = true
build.buildFlag = true
runCallback("OnFrame")

local preAdrenalineMaxStages = build.calcsTab.mainEnv.player.activeSkillList[1].skillModList:Sum("BASE", nil, "Multiplier:BlightMaxStages")
build.configTab.input.buffAdrenaline = true
build.configTab:BuildModList()
runCallback("OnFrame")

assert.True(preAdrenalineMaxStages < build.calcsTab.mainEnv.player.activeSkillList[1].skillModList:Sum("BASE", nil, "Multiplier:BlightMaxStages"))
end)
end)
188 changes: 51 additions & 137 deletions src/Modules/CalcPerform.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,26 @@ local m_huge = math.huge
local bor = bit.bor
local band = bit.band

--- getCachedOutputValue
--- retrieves a value specified by key from a cached version of skill
--- specified by @uuid or if not found in cache computes teh cache.
--- @param env table
--- @param activeSkill table active skill to be used as main when calculating output values
--- @param ... table keys to values to be returned (Note: EmmyLua does not natively support documenting variadic parameters)
--- @return table unpacked table containing the desired values
local function getCachedOutputValue(env, activeSkill, ...)
local uuid = cacheSkillUUID(activeSkill, env)
if not GlobalCache.cachedData[env.mode][uuid] or env.mode == "CALCULATOR" then
calcs.buildActiveSkill(env, env.mode, activeSkill, uuid, {[uuid] = true})
end

local tempValues = {}
for i,v in ipairs({...}) do
tempValues[i] = GlobalCache.cachedData[env.mode][uuid].Env.player.output[v]
end
return unpack(tempValues)
end

-- Merge an instance of a buff, taking the highest value of each modifier
local function mergeBuff(src, destTable, destKey)
if not destTable[destKey] then
Expand Down Expand Up @@ -1808,6 +1828,37 @@ function calcs.perform(env, skipEHP)
end
end

-- To support maximum sustainable stages for the following skills we need to get the data from already
-- computed cached versions to satisfy the order of operations.
-- See: https://github.com/PathOfBuildingCommunity/PathOfBuilding/pull/5164
for _, activeSkill in ipairs(env.player.activeSkillList) do
if not activeSkill.skillFlags.disable and not activeSkill.skillData.limitedProcessing then
if (activeSkill.activeEffect.grantedEffect.name == "Blight" or activeSkill.activeEffect.grantedEffect.name == "Blight of Contagion" or activeSkill.activeEffect.grantedEffect.name == "Blight of Atrophy") and activeSkill.skillPart == 2 then
local rate, duration = getCachedOutputValue(env, activeSkill, "Speed", "Duration")
local baseMaxStages = activeSkill.skillModList:Sum("BASE", env.player.mainSkill.skillCfg, "BlightBaseMaxStages")
local maximum = m_min((m_floor(rate * duration) - 1), baseMaxStages - 1)
activeSkill.skillModList:NewMod("Multiplier:"..activeSkill.activeEffect.grantedEffect.name:gsub("%s+", "").."MaxStages", "BASE", maximum, "Base")
activeSkill.skillModList:NewMod("Multiplier:"..activeSkill.activeEffect.grantedEffect.name:gsub("%s+", "").."StageAfterFirst", "BASE", maximum, "Base")
end
if activeSkill.activeEffect.grantedEffect.name == "Penance Brand of Dissipation" and activeSkill.skillPart == 2 then
local activation_frequency, duration = getCachedOutputValue(env, activeSkill, "HitSpeed", "Duration") -- HitSpeed is the brand activation frequency
local ticks = m_min((m_floor(activation_frequency * duration) - 1), 19)
activeSkill.skillModList:NewMod("Multiplier:PenanceBrandofDissipationMaxStages", "BASE", ticks, "Base")
activeSkill.skillModList:NewMod("Multiplier:PenanceBrandofDissipationStageAfterFirst", "BASE", ticks, "Base")
end
if (activeSkill.activeEffect.grantedEffect.name == "Scorching Ray" or activeSkill.activeEffect.grantedEffect.name == "Scorching Ray of Immolation") and activeSkill.skillPart == 2 then
local maximum = 7
activeSkill.skillModList:NewMod("Multiplier:"..activeSkill.activeEffect.grantedEffect.name:gsub("%s+", "").."MaxStages", "BASE", maximum, "Base")
activeSkill.skillModList:NewMod("Multiplier:"..activeSkill.activeEffect.grantedEffect.name:gsub("%s+", "").."StageAfterFirst", "BASE", maximum, "Base")
end
if (activeSkill.activeEffect.grantedEffect.name == "Earthquake of Amplification") and activeSkill.skillPart == 2 then
local duration = getCachedOutputValue(env, activeSkill, "Duration")
local durationMulti = m_floor(duration * 10)
activeSkill.skillModList:NewMod("Multiplier:100msEarthquakeDuration", "BASE", durationMulti, "Skill:EarthquakeAltX")
end
end
end

local appliedCombustion = false
for _, activeSkill in ipairs(env.player.activeSkillList) do
local skillModList = activeSkill.skillModList
Expand Down Expand Up @@ -2671,143 +2722,6 @@ function calcs.perform(env, skipEHP)
end
end

local function processBuffDebuff(activeSkill)
local skillModList = activeSkill.skillModList
local skillCfg = activeSkill.skillCfg
local newBuffs = {}
local newDebuffs = {}
local newMinionBuffs = {}
for _, buff in ipairs(activeSkill.buffList) do
if buff.cond and not skillModList:GetCondition(buff.cond, skillCfg) then
elseif buff.enemyCond and not enemyDB:GetCondition(buff.enemyCond) then
elseif buff.type == "Buff" and not buffs[buff.name] then
if env.mode_buffs and (not activeSkill.skillFlags.totem or buff.allowTotemBuff) then
local skillCfg = buff.activeSkillBuff and skillCfg
local modStore = buff.activeSkillBuff and skillModList or modDB
if not buff.applyNotPlayer then
activeSkill.buffSkill = true
modDB.conditions["AffectedBy"..buff.name:gsub(" ","")] = true
local srcList = new("ModList")
local inc = modStore:Sum("INC", skillCfg, "BuffEffect", "BuffEffectOnSelf", "BuffEffectOnPlayer") + skillModList:Sum("INC", skillCfg, buff.name:gsub(" ", "").."Effect")
local more = modStore:More(skillCfg, "BuffEffect", "BuffEffectOnSelf")
srcList:ScaleAddList(buff.modList, (1 + inc / 100) * more)
mergeBuff(srcList, buffs, buff.name)
if activeSkill.skillData.thisIsNotABuff then
buffs[buff.name].notBuff = true
end
mergeBuff(srcList, newBuffs, buff.name)
if activeSkill.skillData.thisIsNotABuff then
newBuffs[buff.name].notBuff = true
end
end
if env.minion and (buff.applyMinions or buff.applyAllies) and not minionBuffs[buff.name] then
activeSkill.minionBuffSkill = true
env.minion.modDB.conditions["AffectedBy"..buff.name:gsub(" ","")] = true
local srcList = new("ModList")
local inc = modStore:Sum("INC", skillCfg, "BuffEffect") + env.minion.modDB:Sum("INC", nil, "BuffEffectOnSelf")
local more = modStore:More(skillCfg, "BuffEffect") * env.minion.modDB:More(nil, "BuffEffectOnSelf")
srcList:ScaleAddList(buff.modList, (1 + inc / 100) * more)
mergeBuff(srcList, minionBuffs, buff.name)
mergeBuff(srcList, newMinionBuffs, buff.name)
end
end
elseif (buff.type == "Debuff" or buff.type == "AuraDebuff") and not debuffs[buff.name] then
local stackCount
if buff.stackVar then
stackCount = skillModList:Sum("BASE", skillCfg, "Multiplier:"..buff.stackVar)
if buff.stackLimit then
stackCount = m_min(stackCount, buff.stackLimit)
elseif buff.stackLimitVar then
stackCount = m_min(stackCount, skillModList:Sum("BASE", skillCfg, "Multiplier:"..buff.stackLimitVar))
end
else
stackCount = activeSkill.skillData.stackCount or 1
end
if env.mode_effective and stackCount > 0 then
activeSkill.debuffSkill = true
modDB.conditions["AffectedBy"..buff.name:gsub(" ","")] = true
local srcList = new("ModList")
local mult = 1
if buff.type == "AuraDebuff" then
mult = 0
if not modDB:Flag(nil, "SelfAurasOnlyAffectYou") then
local inc = skillModList:Sum("INC", skillCfg, "AuraEffect", "BuffEffect", "DebuffEffect")
local more = skillModList:More(skillCfg, "AuraEffect", "BuffEffect", "DebuffEffect")
mult = (1 + inc / 100) * more
end
end
if buff.type == "Debuff" then
local inc = skillModList:Sum("INC", skillCfg, "DebuffEffect")
local more = skillModList:More(skillCfg, "DebuffEffect")
mult = (1 + inc / 100) * more
end
srcList:ScaleAddList(buff.modList, mult * stackCount)
if activeSkill.skillData.stackCount or buff.stackVar then
srcList:NewMod("Multiplier:"..buff.name.."Stack", "BASE", stackCount, buff.name)
end
mergeBuff(srcList, debuffs, buff.name)
mergeBuff(srcList, newDebuffs, buff.name)
end
end
end
-- Apply buff/debuff modifiers
for _, modList in pairs(newBuffs) do
modDB:AddList(modList)
if not modList.notBuff then
modDB.multipliers["BuffOnSelf"] = (modDB.multipliers["BuffOnSelf"] or 0) + 1
end
if env.minion then
for _, value in ipairs(modList:List(env.player.mainSkill.skillCfg, "MinionModifier")) do
if not value.type or env.minion.type == value.type then
env.minion.modDB:AddMod(value.mod)
end
end
end
end
if env.minion then
for _, modList in pairs(newMinionBuffs) do
env.minion.modDB:AddList(modList)
end
end
for _, modList in pairs(newDebuffs) do
enemyDB:AddList(modList)
end
end

for _, activeSkill in ipairs(env.player.activeSkillList) do -- Do another pass on the SkillList to catch effects of buffs, if needed
if not activeSkill.skillFlags.disable then
if (activeSkill.activeEffect.grantedEffect.name == "Blight" or activeSkill.activeEffect.grantedEffect.name == "Blight of Contagion" or activeSkill.activeEffect.grantedEffect.name == "Blight of Atrophy") and activeSkill.skillPart == 2 then
local rate = (1 / activeSkill.activeEffect.grantedEffect.castTime) * calcLib.mod(activeSkill.skillModList, activeSkill.skillCfg, "Speed") * calcs.actionSpeedMod(env.player)
local duration = calcSkillDuration(activeSkill.skillModList, activeSkill.skillCfg, activeSkill.skillData, env, enemyDB)
local baseMaxStages = activeSkill.skillModList:Sum("BASE", env.player.mainSkill.skillCfg, "BlightBaseMaxStages")
local maximum = m_min((m_floor(rate * duration) - 1), baseMaxStages - 1)
activeSkill.skillModList:NewMod("Multiplier:"..activeSkill.activeEffect.grantedEffect.name:gsub("%s+", "").."MaxStages", "BASE", maximum, "Base")
activeSkill.skillModList:NewMod("Multiplier:"..activeSkill.activeEffect.grantedEffect.name:gsub("%s+", "").."StageAfterFirst", "BASE", maximum, "Base")
processBuffDebuff(activeSkill)
end
if activeSkill.activeEffect.grantedEffect.name == "Penance Brand of Dissipation" and activeSkill.skillPart == 2 then
local rate = 1 / (activeSkill.skillData.repeatFrequency / (1 + env.player.mainSkill.skillModList:Sum("INC", env.player.mainSkill.skillCfg, "Speed", "BrandActivationFrequency") / 100) / activeSkill.skillModList:More(activeSkill.skillCfg, "BrandActivationFrequency"))
local duration = calcSkillDuration(activeSkill.skillModList, activeSkill.skillCfg, activeSkill.skillData, env, enemyDB)
local ticks = m_min((m_floor(rate * duration) - 1), 19)
activeSkill.skillModList:NewMod("Multiplier:PenanceBrandofDissipationMaxStages", "BASE", ticks, "Base")
activeSkill.skillModList:NewMod("Multiplier:PenanceBrandofDissipationStageAfterFirst", "BASE", ticks, "Base")
processBuffDebuff(activeSkill)
end
if (activeSkill.activeEffect.grantedEffect.name == "Scorching Ray" or activeSkill.activeEffect.grantedEffect.name == "Scorching Ray of Immolation") and activeSkill.skillPart == 2 then
local maximum = 7
activeSkill.skillModList:NewMod("Multiplier:"..activeSkill.activeEffect.grantedEffect.name:gsub("%s+", "").."MaxStages", "BASE", maximum, "Base")
activeSkill.skillModList:NewMod("Multiplier:"..activeSkill.activeEffect.grantedEffect.name:gsub("%s+", "").."StageAfterFirst", "BASE", maximum, "Base")
processBuffDebuff(activeSkill)
end
if (activeSkill.activeEffect.grantedEffect.name == "Earthquake of Amplification") and activeSkill.skillPart == 2 then
local full_duration = calcSkillDuration(activeSkill.skillModList, activeSkill.skillCfg, activeSkill.skillData, env, enemyDB)
local durationMulti = m_floor(full_duration * 10)
activeSkill.skillModList:NewMod("Multiplier:100msEarthquakeDuration", "BASE", durationMulti, "Skill:EarthquakeAltX")
processBuffDebuff(activeSkill)
end
end
end

-- Fix the configured impale stacks on the enemy
-- If the config is missing (blank), then use the maximum number of stacks
-- If the config is larger than the maximum number of stacks, replace it with the correct maximum
Expand Down

0 comments on commit e9dd799

Please sign in to comment.