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
189 changes: 189 additions & 0 deletions spec/System/TestFullDPSAutoTotems_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
describe("TestFullDPSAutoTotems", function()
-- Holy Flame Totem is a direct hit-damage totem skill: its FullDPS contribution
-- comes through `usedEnv.player.output.TotalDPS * activeSkillCount`, which is the
-- exact code path the opt-in scaling targets. A custom mod raises ActiveTotemLimit
-- to 2 so a multiplier > 1 is observable.
local function setupHolyFlameTotemInFullDPS()
newBuild()
build.skillsTab:PasteSocketGroup("Slot: Weapon 1\nHoly Flame Totem 20/0 1\n")
runCallback("OnFrame")
local socketGroup = build.skillsTab.socketGroupList[1]
socketGroup.includeInFullDPS = true
build.configTab.input.customMods = "+1 to maximum number of Summoned Totems"
build.configTab:BuildModList()
build.buildFlag = true
runCallback("OnFrame")
return socketGroup
end

local function equipBow()
build.itemsTab:CreateDisplayItemFromRaw("Test Bow\nShort Bow")
build.itemsTab:AddDisplayItem()
end

local function enableAutoCountWithExtraTotemLimit()
build.configTab.input.customMods = "+2 to maximum number of Summoned Totems"
build.configTab.input.fullDPSAutoTotems = true
build.configTab:BuildModList()
build.buildFlag = true
runCallback("OnFrame")
end

local function assertSkillCount(name, expected)
local matches = 0
for _, entry in ipairs(build.calcsTab.mainOutput.SkillDPS) do
if entry.name == name then
assert.are.equals(expected, entry.count, name .. " Full DPS count")
matches = matches + 1
end
end
assert.is_true(matches > 0, "expected a Full DPS entry for " .. name)
end

it("does not enable the opt-in option by default", function()
newBuild()
assert.is_nil(build.configTab.input.fullDPSAutoTotems)
end)

it("Full DPS for a Totem skill uses skill count 1 when the option is off", function()
setupHolyFlameTotemInFullDPS()
local mainSkill = build.calcsTab.mainEnv.player.mainSkill
assert.is_true(mainSkill.skillFlags.totem)
local baselineFullDPS = build.calcsTab.mainOutput.FullDPS
assert.is_true(baselineFullDPS ~= nil and baselineFullDPS > 0)
local skillDPSEntries = build.calcsTab.mainOutput.SkillDPS
assert.are.equals(1, skillDPSEntries[1].count)
end)

it("Full DPS scales by ActiveTotemLimit when the option is on", function()
setupHolyFlameTotemInFullDPS()
local baselineFullDPS = build.calcsTab.mainOutput.FullDPS

build.configTab.input.fullDPSAutoTotems = true
build.configTab:BuildModList()
build.buildFlag = true
runCallback("OnFrame")

local totemLimit = build.calcsTab.mainOutput.ActiveTotemLimit
assert.is_true(totemLimit > 1, "expected ActiveTotemLimit > 1, got " .. tostring(totemLimit))
-- SkillDPS entry uses the scaled count, which is what comparison tools observe
assert.are.equals(totemLimit, build.calcsTab.mainOutput.SkillDPS[1].count)
-- Combined FullDPS strictly grows; an exact ratio is not asserted because some
-- components (ignite, burning ground) do not scale with totem count.
assert.is_true(build.calcsTab.mainOutput.FullDPS > baselineFullDPS)
end)

it("manual Count > 1 wins over the auto-count option", function()
local socketGroup = setupHolyFlameTotemInFullDPS()
local baselineFullDPS = build.calcsTab.mainOutput.FullDPS

socketGroup.groupCount = 5
build.configTab.input.fullDPSAutoTotems = true
build.configTab:BuildModList()
build.buildFlag = true
runCallback("OnFrame")

local totemLimit = build.calcsTab.mainOutput.ActiveTotemLimit
assert.is_true(totemLimit ~= 5, "test relies on ActiveTotemLimit being different from 5, got " .. tostring(totemLimit))
assert.are.equals(5, build.calcsTab.mainOutput.SkillDPS[1].count)
assert.is_true(build.calcsTab.mainOutput.FullDPS > baselineFullDPS)
end)

it("does not auto-scale when multiple Totem skills are included in Full DPS (avoids counting the global limit twice)", function()
-- Two distinct Totem socket groups both opted into Full DPS, both at Count 1.
-- ActiveTotemLimit is a global slot pool; applying it to each skill would
-- multi-count the same totem slots. The implementation must keep each skill
-- at its manual Count when more than one Totem source is included.
newBuild()
build.skillsTab:PasteSocketGroup("Slot: Weapon 1\nHoly Flame Totem 20/0 1\n")
runCallback("OnFrame")
build.skillsTab.socketGroupList[1].includeInFullDPS = true

build.skillsTab:PasteSocketGroup("Slot: Body Armour\nHoly Flame Totem 20/0 1\n")
runCallback("OnFrame")
build.skillsTab.socketGroupList[2].includeInFullDPS = true

enableAutoCountWithExtraTotemLimit()

local totemLimit = build.calcsTab.mainOutput.ActiveTotemLimit
assert.is_true(totemLimit > 1, "expected ActiveTotemLimit > 1, got " .. tostring(totemLimit))

local totemEntries = 0
for _, entry in ipairs(build.calcsTab.mainOutput.SkillDPS) do
if entry.name == "Holy Flame Totem" then
assert.are.equals(1, entry.count, "Holy Flame Totem entry must stay at count 1 when multiple totem sources are included")
totemEntries = totemEntries + 1
end
end
assert.are.equals(2, totemEntries, "expected both Holy Flame Totem socket groups in the Full DPS skill list")
end)

it("ignores disabled Vaal Totem skills when counting Full DPS sources", function()
setupHolyFlameTotemInFullDPS()
build.skillsTab:PasteSocketGroup("Slot: Body Armour\nVaal Rejuvenation Totem 20/0 1\n")
runCallback("OnFrame")
local socketGroup = build.skillsTab.socketGroupList[2]
socketGroup.includeInFullDPS = true
socketGroup.gemList[1].enableGlobal1 = false
socketGroup.gemList[1].enableGlobal2 = false
build.configTab.input.fullDPSAutoTotems = true
build.configTab:BuildModList()
build.buildFlag = true
runCallback("OnFrame")

local totemLimit = build.calcsTab.mainOutput.ActiveTotemLimit
assert.is_true(totemLimit > 1, "expected ActiveTotemLimit > 1, got " .. tostring(totemLimit))
assertSkillCount("Holy Flame Totem", totemLimit)
end)

it("does not auto-scale Explosive Arrow Ballista", function()
newBuild()
equipBow()
build.skillsTab:PasteSocketGroup("Slot: Weapon 1\nExplosive Arrow 20/0 1\nBallista Totem 20/0 1\n")
runCallback("OnFrame")
build.skillsTab.socketGroupList[1].includeInFullDPS = true
enableAutoCountWithExtraTotemLimit()

assert.is_true(build.calcsTab.mainOutput.ActiveTotemLimit > 1)
assertSkillCount("Explosive Arrow", 1)
end)

it("counts Explosive Arrow as a second Totem source", function()
newBuild()
equipBow()
build.skillsTab:PasteSocketGroup("Slot: Weapon 1\nExplosive Arrow 20/0 1\nBallista Totem 20/0 1\n")
runCallback("OnFrame")
build.skillsTab.socketGroupList[1].includeInFullDPS = true

build.skillsTab:PasteSocketGroup("Slot: Body Armour\nHoly Flame Totem 20/0 1\n")
runCallback("OnFrame")
build.skillsTab.socketGroupList[2].includeInFullDPS = true
enableAutoCountWithExtraTotemLimit()

assert.is_true(build.calcsTab.mainOutput.ActiveTotemLimit > 1)
assertSkillCount("Explosive Arrow", 1)
assertSkillCount("Holy Flame Totem", 1)
end)

it("uses the current TotemsSummoned override, not ActiveTotemLimit, when both are set", function()
-- Raise ActiveTotemLimit to 4 via custom mod, then set the existing TotemsSummoned
-- config to 2: the Full DPS count reads output.TotemsSummoned first, so it must
-- land on 2, not 4. This pins the "current count" half of the tooltip contract.
newBuild()
build.skillsTab:PasteSocketGroup("Slot: Weapon 1\nHoly Flame Totem 20/0 1\n")
runCallback("OnFrame")
local socketGroup = build.skillsTab.socketGroupList[1]
socketGroup.includeInFullDPS = true
build.configTab.input.customMods = "+3 to maximum number of Summoned Totems"
build.configTab.input.TotemsSummoned = 2
build.configTab.input.fullDPSAutoTotems = true
build.configTab:BuildModList()
build.buildFlag = true
runCallback("OnFrame")

local totemLimit = build.calcsTab.mainOutput.ActiveTotemLimit
assert.is_true(totemLimit > 2, "expected ActiveTotemLimit > TotemsSummoned override, got " .. tostring(totemLimit))
assert.are.equals(2, build.calcsTab.mainOutput.TotemsSummoned)
assert.are.equals(2, build.calcsTab.mainOutput.SkillDPS[1].count)
end)
end)
67 changes: 66 additions & 1 deletion src/Modules/Calcs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,68 @@ local function getActiveSkillCount(activeSkill)
return 1, true
end

local fullDPSTotemPoolKey = { }
local fullDPSTotemCountPolicy = {
getPoolKey = function(activeSkill, enabled)
-- Explosive Arrow Ballista occupies the same global slot pool as other Totem
-- skills even though its custom DPS function already models active Totems.
if enabled
and activeSkill.socketGroup
and activeSkill.socketGroup.includeInFullDPS
and activeSkill.skillFlags
and activeSkill.skillFlags.totem == true then
return fullDPSTotemPoolKey
end
end,
resolveCount = function(env, activeSkill, sourceCount)
-- TotemsSummoned / ActiveTotemLimit is a global slot pool. With more than one
-- participating source (including Explosive Arrow), allocating it to one skill
-- would be ambiguous, so the manual Count remains authoritative.
if not env.configInput.fullDPSAutoTotems
or sourceCount ~= 1
or activeSkill.activeEffect.grantedEffect.explosiveArrowFunc then
return
end
local output = env.player.output
return output.TotemsSummoned or output.ActiveTotemLimit
end,
}

-- A policy returns the same pool key for every source that shares a limited pool.
-- resolveCount returns an automatic Count or nil to keep the manual Count. Policies
-- are ordered; the first policy that returns a pool key owns the skill.
local fullDPSCountPolicies = {
fullDPSTotemCountPolicy,
}

local function buildFullDPSCountContext(activeSkillList)
local context = { sourceCountByPoolKey = { } }
for _, activeSkill in ipairs(activeSkillList) do
local _, enabled = getActiveSkillCount(activeSkill)
for _, policy in ipairs(fullDPSCountPolicies) do
local poolKey = policy.getPoolKey(activeSkill, enabled)
if poolKey then
context.sourceCountByPoolKey[poolKey] = (context.sourceCountByPoolKey[poolKey] or 0) + 1
break
end
end
end
return context
end

local function resolveFullDPSCount(env, activeSkill, activeSkillCount, context)
if activeSkillCount ~= 1 then
return activeSkillCount
end
for _, policy in ipairs(fullDPSCountPolicies) do
local poolKey = policy.getPoolKey(activeSkill, true)
if poolKey then
return policy.resolveCount(env, activeSkill, context.sourceCountByPoolKey[poolKey] or 0) or activeSkillCount
end
end
return activeSkillCount
end

function calcs.calcFullDPS(build, mode, override, specEnv)
local fullEnv, cachedPlayerDB, cachedEnemyDB, cachedMinionDB = calcs.initEnv(build, mode, override, specEnv)
local usedEnv = nil
Expand All @@ -199,14 +261,17 @@ function calcs.calcFullDPS(build, mode, override, specEnv)
local igniteSource = ""
local burningGroundSource = ""
local causticGroundSource = ""


local fullDPSCountContext = buildFullDPSCountContext(fullEnv.player.activeSkillList)

for _, activeSkill in ipairs(fullEnv.player.activeSkillList) do
if activeSkill.socketGroup and activeSkill.socketGroup.includeInFullDPS then
local activeSkillCount, enabled = getActiveSkillCount(activeSkill)
if enabled then
fullEnv.player.mainSkill = activeSkill
calcs.perform(fullEnv, true)
usedEnv = fullEnv
activeSkillCount = resolveFullDPSCount(fullEnv, activeSkill, activeSkillCount, fullDPSCountContext)
local minionName = nil
if activeSkill.minion or usedEnv.minion then
if usedEnv.minion.output.TotalDPS and usedEnv.minion.output.TotalDPS > 0 then
Expand Down
13 changes: 13 additions & 0 deletions src/Modules/ConfigOptions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1113,6 +1113,19 @@ Huge sets the radius to 11.
modList:NewMod("TotemsSummoned", "OVERRIDE", val, "Config", { type = "Condition", var = "Combat" })
modList:NewMod("Condition:HaveTotem", "FLAG", val >= 1, "Config", { type = "Condition", var = "Combat" })
end },
{
var = "fullDPSAutoTotems",
type = "check",
label = "Auto-count Totems in Full DPS?",
ifSkillFlag = "totem",
tooltip =
"If enabled, Full DPS will use your current number of Summoned Totems for Totem skills\n"
.. "when their skill Count is 1.\n\n"
.. "Manual Count values greater than 1 are still respected.\n\n"
.. "Only applies when a single Totem skill is included in Full DPS. With multiple\n"
.. "Totem skills, the global totem-slot pool cannot be allocated automatically and\n"
.. "manual Count is required for each.",
},
{ var = "conditionSummonedGolemInPast8Sec", type = "check", label = "Summoned Golem in past 8 Seconds?", ifCond = "SummonedGolemInPast8Sec", implyCond = "SummonedGolemInPast10Sec", apply = function(val, modList, enemyModList)
modList:NewMod("Condition:SummonedGolemInPast8Sec", "FLAG", true, "Config", { type = "Condition", var = "Combat" })
end },
Expand Down
Loading