Skip to content

Commit 708ce9d

Browse files
mcagnionclaude
andcommitted
Add opt-in Full DPS auto-count for Totem skills
Add a Configuration option "Auto-count Totems in Full DPS?" that lets Full DPS treat a Totem skill's count as the build's current TotemsSummoned (falling back to ActiveTotemLimit) when the skill's manual Count is 1. Behavior: - Default off: Full DPS uses the manual skill Count, unchanged. - Option on, Count == 1, skill has skillFlags.totem, not Explosive Arrow, and exactly one Totem source is included in Full DPS: Full DPS uses output.TotemsSummoned or output.ActiveTotemLimit. - Option on, Count > 1: manual Count wins. - Explosive Arrow is excluded from scaling because its custom DPS function already accounts for active totems, but it still counts as a Full DPS totem source for the multi-source guard. - Two or more Totem sources included in Full DPS (including Explosive Arrow): auto-count is suppressed and each skill keeps its manual Count, because ActiveTotemLimit is a global slot pool that cannot be allocated automatically across multiple sources. The implementation keeps two predicates intentionally separate: isIncludedFullDPSTotemSource (broader, used by the source counter) matches every included Totem skill that occupies a global totem slot; isFullDPSAutoTotemScalable (narrower, used by the per-skill scaling gate) additionally excludes Explosive Arrow. The option is gated by ifSkillFlag = "totem" so it only appears for builds containing at least one Totem skill, making it useful for comparison tools (Power Report, Compare tab, anoint sorting, trade query) to surface "+1 to maximum number of Summoned Totems" sources in single-totem-source Full DPS setups. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0fc3283 commit 708ce9d

3 files changed

Lines changed: 205 additions & 1 deletion

File tree

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
describe("TestFullDPSAutoTotems", function()
2+
-- Holy Flame Totem is a direct hit-damage totem skill: its FullDPS contribution
3+
-- comes through `usedEnv.player.output.TotalDPS * activeSkillCount`, which is the
4+
-- exact code path the opt-in scaling targets. A custom mod raises ActiveTotemLimit
5+
-- to 2 so a multiplier > 1 is observable.
6+
local function setupHolyFlameTotemInFullDPS()
7+
newBuild()
8+
build.skillsTab:PasteSocketGroup("Slot: Weapon 1\nHoly Flame Totem 20/0 1\n")
9+
runCallback("OnFrame")
10+
local socketGroup = build.skillsTab.socketGroupList[1]
11+
socketGroup.includeInFullDPS = true
12+
build.configTab.input.customMods = "+1 to maximum number of Summoned Totems"
13+
build.configTab:BuildModList()
14+
build.buildFlag = true
15+
runCallback("OnFrame")
16+
return socketGroup
17+
end
18+
19+
teardown(function()
20+
-- newBuild() resets state for the next describe block
21+
end)
22+
23+
it("does not enable the opt-in option by default", function()
24+
newBuild()
25+
assert.is_nil(build.configTab.input.fullDPSAutoMaxTotems)
26+
end)
27+
28+
it("Full DPS for a Totem skill uses skill count 1 when the option is off", function()
29+
setupHolyFlameTotemInFullDPS()
30+
local mainSkill = build.calcsTab.mainEnv.player.mainSkill
31+
assert.is_true(mainSkill.skillFlags.totem)
32+
local baselineFullDPS = build.calcsTab.mainOutput.FullDPS
33+
assert.is_true(baselineFullDPS ~= nil and baselineFullDPS > 0)
34+
local skillDPSEntries = build.calcsTab.mainOutput.SkillDPS
35+
assert.are.equals(1, skillDPSEntries[1].count)
36+
end)
37+
38+
it("Full DPS scales by ActiveTotemLimit when the option is on", function()
39+
setupHolyFlameTotemInFullDPS()
40+
local baselineFullDPS = build.calcsTab.mainOutput.FullDPS
41+
42+
build.configTab.input.fullDPSAutoMaxTotems = true
43+
build.configTab:BuildModList()
44+
build.buildFlag = true
45+
runCallback("OnFrame")
46+
47+
local totemLimit = build.calcsTab.mainOutput.ActiveTotemLimit
48+
assert.is_true(totemLimit > 1, "expected ActiveTotemLimit > 1, got " .. tostring(totemLimit))
49+
-- SkillDPS entry uses the scaled count, which is what comparison tools observe
50+
assert.are.equals(totemLimit, build.calcsTab.mainOutput.SkillDPS[1].count)
51+
-- Combined FullDPS strictly grows; an exact ratio is not asserted because some
52+
-- components (ignite, burning ground) do not scale with totem count.
53+
assert.is_true(build.calcsTab.mainOutput.FullDPS > baselineFullDPS)
54+
end)
55+
56+
it("manual Count > 1 wins over the auto-count option", function()
57+
local socketGroup = setupHolyFlameTotemInFullDPS()
58+
local baselineFullDPS = build.calcsTab.mainOutput.FullDPS
59+
60+
socketGroup.groupCount = 5
61+
build.configTab.input.fullDPSAutoMaxTotems = true
62+
build.configTab:BuildModList()
63+
build.buildFlag = true
64+
runCallback("OnFrame")
65+
66+
local totemLimit = build.calcsTab.mainOutput.ActiveTotemLimit
67+
assert.is_true(totemLimit ~= 5, "test relies on ActiveTotemLimit being different from 5, got " .. tostring(totemLimit))
68+
assert.are.equals(5, build.calcsTab.mainOutput.SkillDPS[1].count)
69+
assert.is_true(build.calcsTab.mainOutput.FullDPS > baselineFullDPS)
70+
end)
71+
72+
it("does not auto-scale when multiple Totem skills are included in Full DPS (avoids overcounting the global limit)", function()
73+
-- Two distinct Totem socket groups both opted into Full DPS, both at Count 1.
74+
-- ActiveTotemLimit is a global slot pool; applying it to each skill would
75+
-- multi-count the same totem slots. The implementation must keep each skill
76+
-- at its manual Count when more than one Totem source is included.
77+
--
78+
-- Explosive Arrow Ballista in the same scenario is handled correctly by
79+
-- construction in `src/Modules/Calcs.lua`: `isIncludedFullDPSTotemSource`
80+
-- (used by the source counter) does NOT check `explosiveArrowFunc`, so an
81+
-- EA Ballista source still increments the source count; only
82+
-- `isFullDPSAutoTotemScalable` (used by the per-skill scaling gate) excludes
83+
-- it. The two predicates cannot be conflated without editing the helpers
84+
-- themselves. A spec-level test for the EA Ballista variant would require
85+
-- additional weapon+support fixture wiring that the existing test harness
86+
-- does not currently expose.
87+
newBuild()
88+
build.skillsTab:PasteSocketGroup("Slot: Weapon 1\nHoly Flame Totem 20/0 1\n")
89+
runCallback("OnFrame")
90+
build.skillsTab.socketGroupList[1].includeInFullDPS = true
91+
92+
build.skillsTab:PasteSocketGroup("Slot: Body Armour\nHoly Flame Totem 20/0 1\n")
93+
runCallback("OnFrame")
94+
build.skillsTab.socketGroupList[2].includeInFullDPS = true
95+
96+
build.configTab.input.customMods = "+2 to maximum number of Summoned Totems"
97+
build.configTab.input.fullDPSAutoMaxTotems = true
98+
build.configTab:BuildModList()
99+
build.buildFlag = true
100+
runCallback("OnFrame")
101+
102+
local totemLimit = build.calcsTab.mainOutput.ActiveTotemLimit
103+
assert.is_true(totemLimit > 1, "expected ActiveTotemLimit > 1, got " .. tostring(totemLimit))
104+
105+
local totemEntries = 0
106+
for _, entry in ipairs(build.calcsTab.mainOutput.SkillDPS) do
107+
if entry.name == "Holy Flame Totem" then
108+
assert.are.equals(1, entry.count, "Holy Flame Totem entry must stay at count 1 when multiple totem sources are included")
109+
totemEntries = totemEntries + 1
110+
end
111+
end
112+
assert.are.equals(2, totemEntries, "expected both Holy Flame Totem socket groups in the Full DPS skill list")
113+
end)
114+
115+
it("uses the current TotemsSummoned override, not ActiveTotemLimit, when both are set", function()
116+
-- Raise ActiveTotemLimit to 4 via custom mod, then set the existing TotemsSummoned
117+
-- config to 2: getSummonedTotemCount reads output.TotemsSummoned first, so it must
118+
-- land on 2, not 4. This pins the "current count" half of the tooltip contract.
119+
newBuild()
120+
build.skillsTab:PasteSocketGroup("Slot: Weapon 1\nHoly Flame Totem 20/0 1\n")
121+
runCallback("OnFrame")
122+
local socketGroup = build.skillsTab.socketGroupList[1]
123+
socketGroup.includeInFullDPS = true
124+
build.configTab.input.customMods = "+3 to maximum number of Summoned Totems"
125+
build.configTab.input.TotemsSummoned = 2
126+
build.configTab.input.fullDPSAutoMaxTotems = true
127+
build.configTab:BuildModList()
128+
build.buildFlag = true
129+
runCallback("OnFrame")
130+
131+
local totemLimit = build.calcsTab.mainOutput.ActiveTotemLimit
132+
assert.is_true(totemLimit > 2, "expected ActiveTotemLimit > TotemsSummoned override, got " .. tostring(totemLimit))
133+
assert.are.equals(2, build.calcsTab.mainOutput.TotemsSummoned)
134+
assert.are.equals(2, build.calcsTab.mainOutput.SkillDPS[1].count)
135+
end)
136+
end)

src/Modules/Calcs.lua

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,56 @@ local function getActiveSkillCount(activeSkill)
173173
return 1, true
174174
end
175175

176+
-- A Full DPS totem skill occupies a slot in the global totem-slot pool, regardless
177+
-- of whether the generic auto-count operation is allowed to scale it. Explosive
178+
-- Arrow Ballista is the notable case: its custom DPS function already models active
179+
-- totems internally, so it must not be scaled again -- but it still consumes a
180+
-- global totem slot and therefore counts as a Full DPS totem source.
181+
local function isIncludedFullDPSTotemSource(activeSkill)
182+
if not activeSkill.socketGroup or not activeSkill.socketGroup.includeInFullDPS then
183+
return false
184+
end
185+
return activeSkill.skillFlags and activeSkill.skillFlags.totem == true
186+
end
187+
188+
local function isFullDPSAutoTotemScalable(activeSkill)
189+
if not isIncludedFullDPSTotemSource(activeSkill) then
190+
return false
191+
end
192+
-- Explosive Arrow already accounts for active totems in its custom DPS logic.
193+
return not activeSkill.activeEffect.grantedEffect.explosiveArrowFunc
194+
end
195+
196+
local function countFullDPSTotemSources(activeSkillList)
197+
local count = 0
198+
for _, activeSkill in ipairs(activeSkillList) do
199+
if isIncludedFullDPSTotemSource(activeSkill) then
200+
count = count + 1
201+
end
202+
end
203+
return count
204+
end
205+
206+
local function shouldScaleFullDPSBySummonedTotems(env, activeSkill, activeSkillCount, totemSourceCount)
207+
if not env.configInput.fullDPSAutoMaxTotems then
208+
return false
209+
end
210+
if activeSkillCount ~= 1 then
211+
return false
212+
end
213+
if not isFullDPSAutoTotemScalable(activeSkill) then
214+
return false
215+
end
216+
-- ActiveTotemLimit / TotemsSummoned is a global slot pool. With more than one
217+
-- Totem source included in Full DPS (including Explosive Arrow), applying it to
218+
-- each scalable skill would overcount; fall back to manual Count for the user.
219+
return totemSourceCount == 1
220+
end
221+
222+
local function getSummonedTotemCount(output)
223+
return output.TotemsSummoned or output.ActiveTotemLimit or 1
224+
end
225+
176226
function calcs.calcFullDPS(build, mode, override, specEnv)
177227
local fullEnv, cachedPlayerDB, cachedEnemyDB, cachedMinionDB = calcs.initEnv(build, mode, override, specEnv)
178228
local usedEnv = nil
@@ -198,14 +248,19 @@ function calcs.calcFullDPS(build, mode, override, specEnv)
198248
local igniteSource = ""
199249
local burningGroundSource = ""
200250
local causticGroundSource = ""
201-
251+
252+
local fullDPSAutoTotemSourceCount = countFullDPSTotemSources(fullEnv.player.activeSkillList)
253+
202254
for _, activeSkill in ipairs(fullEnv.player.activeSkillList) do
203255
if activeSkill.socketGroup and activeSkill.socketGroup.includeInFullDPS then
204256
local activeSkillCount, enabled = getActiveSkillCount(activeSkill)
205257
if enabled then
206258
fullEnv.player.mainSkill = activeSkill
207259
calcs.perform(fullEnv, true)
208260
usedEnv = fullEnv
261+
if shouldScaleFullDPSBySummonedTotems(fullEnv, activeSkill, activeSkillCount, fullDPSAutoTotemSourceCount) then
262+
activeSkillCount = getSummonedTotemCount(usedEnv.player.output)
263+
end
209264
local minionName = nil
210265
if activeSkill.minion or usedEnv.minion then
211266
if usedEnv.minion.output.TotalDPS and usedEnv.minion.output.TotalDPS > 0 then

src/Modules/ConfigOptions.lua

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,6 +1099,19 @@ Huge sets the radius to 11.
10991099
modList:NewMod("TotemsSummoned", "OVERRIDE", val, "Config", { type = "Condition", var = "Combat" })
11001100
modList:NewMod("Condition:HaveTotem", "FLAG", val >= 1, "Config", { type = "Condition", var = "Combat" })
11011101
end },
1102+
{
1103+
var = "fullDPSAutoMaxTotems",
1104+
type = "check",
1105+
label = "Auto-count Totems in Full DPS?",
1106+
ifSkillFlag = "totem",
1107+
tooltip =
1108+
"If enabled, Full DPS will use your current number of Summoned Totems for Totem skills\n"
1109+
.. "when their skill Count is 1.\n\n"
1110+
.. "Manual Count values greater than 1 are still respected.\n\n"
1111+
.. "Only applies when a single Totem skill is included in Full DPS. With multiple\n"
1112+
.. "Totem skills, the global totem-slot pool cannot be allocated automatically and\n"
1113+
.. "manual Count is required for each.",
1114+
},
11021115
{ var = "conditionSummonedGolemInPast8Sec", type = "check", label = "Summoned Golem in past 8 Seconds?", ifCond = "SummonedGolemInPast8Sec", implyCond = "SummonedGolemInPast10Sec", apply = function(val, modList, enemyModList)
11031116
modList:NewMod("Condition:SummonedGolemInPast8Sec", "FLAG", true, "Config", { type = "Condition", var = "Combat" })
11041117
end },

0 commit comments

Comments
 (0)