diff --git a/spec/System/TestSkills_spec.lua b/spec/System/TestSkills_spec.lua index 7b2fff1f57..d7ec46ec3c 100644 --- a/spec/System/TestSkills_spec.lua +++ b/spec/System/TestSkills_spec.lua @@ -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) \ No newline at end of file diff --git a/src/Modules/CalcPerform.lua b/src/Modules/CalcPerform.lua index 5d2919c700..380606f630 100644 --- a/src/Modules/CalcPerform.lua +++ b/src/Modules/CalcPerform.lua @@ -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 @@ -1778,6 +1798,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 @@ -2641,143 +2692,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