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
136 changes: 136 additions & 0 deletions spec/System/TestFullDPSAutoTotems_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
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

teardown(function()
-- newBuild() resets state for the next describe block
end)

it("does not enable the opt-in option by default", function()
newBuild()
assert.is_nil(build.configTab.input.fullDPSAutoMaxTotems)
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.fullDPSAutoMaxTotems = 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.fullDPSAutoMaxTotems = 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 overcounting the global limit)", 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.
--
-- Explosive Arrow Ballista in the same scenario is handled correctly by
-- construction in `src/Modules/Calcs.lua`: `isIncludedFullDPSTotemSource`
-- (used by the source counter) does NOT check `explosiveArrowFunc`, so an
-- EA Ballista source still increments the source count; only
-- `isFullDPSAutoTotemScalable` (used by the per-skill scaling gate) excludes
-- it. The two predicates cannot be conflated without editing the helpers
-- themselves. A spec-level test for the EA Ballista variant would require
-- additional weapon+support fixture wiring that the existing test harness
-- does not currently expose.
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

build.configTab.input.customMods = "+2 to maximum number of Summoned Totems"
build.configTab.input.fullDPSAutoMaxTotems = 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))

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("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: getSummonedTotemCount 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.fullDPSAutoMaxTotems = 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)
57 changes: 56 additions & 1 deletion src/Modules/Calcs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,56 @@ local function getActiveSkillCount(activeSkill)
return 1, true
end

-- A Full DPS totem skill occupies a slot in the global totem-slot pool, regardless
-- of whether the generic auto-count operation is allowed to scale it. Explosive
-- Arrow Ballista is the notable case: its custom DPS function already models active
-- totems internally, so it must not be scaled again -- but it still consumes a
-- global totem slot and therefore counts as a Full DPS totem source.
local function isIncludedFullDPSTotemSource(activeSkill)
if not activeSkill.socketGroup or not activeSkill.socketGroup.includeInFullDPS then
return false
end
return activeSkill.skillFlags and activeSkill.skillFlags.totem == true
end

local function isFullDPSAutoTotemScalable(activeSkill)
if not isIncludedFullDPSTotemSource(activeSkill) then
return false
end
-- Explosive Arrow already accounts for active totems in its custom DPS logic.
return not activeSkill.activeEffect.grantedEffect.explosiveArrowFunc
end

local function countFullDPSTotemSources(activeSkillList)
local count = 0
for _, activeSkill in ipairs(activeSkillList) do
if isIncludedFullDPSTotemSource(activeSkill) then
count = count + 1
end
end
return count
end

local function shouldScaleFullDPSBySummonedTotems(env, activeSkill, activeSkillCount, totemSourceCount)
if not env.configInput.fullDPSAutoMaxTotems then
return false
end
if activeSkillCount ~= 1 then
return false
end
if not isFullDPSAutoTotemScalable(activeSkill) then
return false
end
-- ActiveTotemLimit / TotemsSummoned is a global slot pool. With more than one
-- Totem source included in Full DPS (including Explosive Arrow), applying it to
-- each scalable skill would overcount; fall back to manual Count for the user.
return totemSourceCount == 1
end

local function getSummonedTotemCount(output)
return output.TotemsSummoned or output.ActiveTotemLimit or 1
end

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


local fullDPSAutoTotemSourceCount = countFullDPSTotemSources(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
if shouldScaleFullDPSBySummonedTotems(fullEnv, activeSkill, activeSkillCount, fullDPSAutoTotemSourceCount) then
activeSkillCount = getSummonedTotemCount(usedEnv.player.output)
end
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 @@ -1099,6 +1099,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 = "fullDPSAutoMaxTotems",
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