Skip to content

Commit 499aa18

Browse files
SaloEaterLocalIdentity
andauthored
[3.29] New witch notables (#9964)
* feature: Roiling Tempest notable * feature: Bitter Frost notable * feature: Voracious Flame notable * chore: test order * fix: calcs * chore: pipeline * fix: new wording * Simplify ModParser * Change ordering --------- Co-authored-by: LocalIdentity <localidentity2@gmail.com>
1 parent 2f7283d commit 499aa18

5 files changed

Lines changed: 307 additions & 62 deletions

File tree

spec/System/TestOffence_spec.lua

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
describe("TestOffence", function()
2+
before_each(function()
3+
newBuild()
4+
end)
5+
6+
teardown(function()
7+
-- newBuild() takes care of resetting everything in setup()
8+
end)
9+
10+
-- Asserts actual is within a relative tolerance of expected, e.g. 0.005 = 0.5%
11+
local function assertNearRelative(expected, actual, tolerance, msg)
12+
assert.is_true(math.abs(expected - actual) / expected <= tolerance,
13+
string.format("%s: expected ~%.2f (within %.1f%%), got %.2f", msg, expected, tolerance * 100, actual))
14+
end
15+
16+
it("parses more/less/increased/reduced minimum and maximum damage of every type", function()
17+
build.itemsTab:CreateDisplayItemFromRaw([[
18+
New Item
19+
Coral Amulet
20+
25% more Maximum Lightning Damage
21+
50% less Minimum Lightning Damage
22+
10% increased Maximum Cold Damage
23+
12% reduced Minimum Physical Damage
24+
20% more Maximum Chaos Damage
25+
30% more Maximum Fire Damage
26+
]])
27+
build.itemsTab:AddDisplayItem()
28+
runCallback("OnFrame")
29+
30+
local modDB = build.calcsTab.mainEnv.player.modDB
31+
32+
-- "more"/"less" -> MORE modifier (More() returns the aggregate multiplier)
33+
assert.are.equals(1.25, modDB:More(nil, "MaxLightningDamage"))
34+
assert.are.equals(0.5, modDB:More(nil, "MinLightningDamage"))
35+
assert.are.equals(1.2, modDB:More(nil, "MaxChaosDamage"))
36+
assert.are.equals(1.3, modDB:More(nil, "MaxFireDamage"))
37+
38+
-- "increased"/"reduced" -> INC modifier
39+
assert.are.equals(10, modDB:Sum("INC", nil, "MaxColdDamage"))
40+
assert.are.equals(-12, modDB:Sum("INC", nil, "MinPhysicalDamage"))
41+
42+
-- sanity: these must not have leaked into the wrong stat/type
43+
assert.are.equals(1, modDB:More(nil, "MinChaosDamage"))
44+
assert.are.equals(0, modDB:Sum("INC", nil, "MaxLightningDamage"))
45+
end)
46+
47+
-- calcDamage rounds each min/max to an integer before our multipliers' effects can be
48+
-- observed, so scaling the rounded baseline can differ from the real value by ~1
49+
local function assertNear(expected, actual, msg)
50+
assert.is_true(math.abs(expected - actual) <= 2, string.format("%s: expected ~%.2f, got %.2f", msg, expected, actual))
51+
end
52+
53+
it("applies min/max damage mods to an actual skill in CalcOffence", function()
54+
build.skillsTab:PasteSocketGroup("Slot: Body Armour\nArc 20/0 1\n")
55+
runCallback("OnFrame")
56+
57+
local baseMin = build.calcsTab.calcsOutput.LightningMin
58+
local baseMax = build.calcsTab.calcsOutput.LightningMax
59+
assert.is_true(baseMax > 0)
60+
assert.is_true(baseMin > 0)
61+
62+
-- MORE path: "more"/"less" scale only the targeted end of the roll
63+
build.itemsTab:CreateDisplayItemFromRaw([[
64+
New Item
65+
Coral Amulet
66+
25% more Maximum Lightning Damage
67+
50% less Minimum Lightning Damage
68+
]])
69+
build.itemsTab:AddDisplayItem()
70+
runCallback("OnFrame")
71+
72+
assertNear(baseMax * 1.25, build.calcsTab.calcsOutput.LightningMax, "25%% more max")
73+
assertNear(baseMin * 0.5, build.calcsTab.calcsOutput.LightningMin, "50%% less min")
74+
75+
-- INC path: "increased" stacks multiplicatively with the MORE factors above
76+
build.itemsTab:CreateDisplayItemFromRaw([[
77+
New Item
78+
Coral Ring
79+
40% increased Maximum Lightning Damage
80+
100% increased Minimum Lightning Damage
81+
]])
82+
build.itemsTab:AddDisplayItem()
83+
runCallback("OnFrame")
84+
85+
assertNear(baseMax * 1.25 * 1.4, build.calcsTab.calcsOutput.LightningMax, "more + increased max")
86+
assertNear(baseMin * 0.5 * 2, build.calcsTab.calcsOutput.LightningMin, "less + increased min")
87+
end)
88+
89+
it("applies minimum and maximum attack damage mods", function()
90+
build.itemsTab:CreateDisplayItemFromRaw([[
91+
New Item
92+
Rusted Sword
93+
]])
94+
build.itemsTab:AddDisplayItem()
95+
build.skillsTab:PasteSocketGroup("Double Strike 20/0 1")
96+
runCallback("OnFrame")
97+
98+
local baseMin = build.calcsTab.mainOutput.MainHand.TotalMin
99+
local baseMax = build.calcsTab.mainOutput.MainHand.TotalMax
100+
101+
build.itemsTab:CreateDisplayItemFromRaw([[
102+
New Item
103+
Coral Amulet
104+
50% less Minimum Attack Damage
105+
100% more Maximum Attack Damage
106+
]])
107+
build.itemsTab:AddDisplayItem()
108+
runCallback("OnFrame")
109+
110+
assertNear(baseMin * 0.5, build.calcsTab.mainOutput.MainHand.TotalMin, "50%% less attack min")
111+
assertNear(baseMax * 2, build.calcsTab.mainOutput.MainHand.TotalMax, "100%% more attack max")
112+
end)
113+
114+
it("parses universal cannot deal/deal no non-<type> damage for player and minions", function()
115+
build.skillsTab:PasteSocketGroup("Slot: Body Armour\nArc 20/0 1\n")
116+
runCallback("OnFrame")
117+
assert.is_true(build.calcsTab.calcsOutput.LightningMax > 0)
118+
119+
build.itemsTab:CreateDisplayItemFromRaw([[
120+
New Item
121+
Coral Amulet
122+
Cannot deal non-Fire Damage
123+
Minions deal no non-Fire Damage
124+
]])
125+
build.itemsTab:AddDisplayItem()
126+
runCallback("OnFrame")
127+
128+
local modDB = build.calcsTab.mainEnv.player.modDB
129+
assert.truthy(modDB:Flag(nil, "DealNoPhysical"))
130+
assert.truthy(modDB:Flag(nil, "DealNoLightning"))
131+
assert.truthy(modDB:Flag(nil, "DealNoCold"))
132+
assert.truthy(modDB:Flag(nil, "DealNoChaos"))
133+
assert.is_true(not (modDB:Flag(nil, "DealNoFire")))
134+
135+
-- minion wording wraps the same flags in MinionModifier
136+
local minionDealNo = { }
137+
for _, value in ipairs(modDB:List(nil, "MinionModifier")) do
138+
if value.mod and value.mod.name:match("^DealNo") then
139+
minionDealNo[value.mod.name] = true
140+
end
141+
end
142+
assert.truthy(minionDealNo["DealNoPhysical"])
143+
assert.truthy(minionDealNo["DealNoLightning"])
144+
assert.truthy(minionDealNo["DealNoCold"])
145+
assert.truthy(minionDealNo["DealNoChaos"])
146+
assert.is_true(not (minionDealNo["DealNoFire"]))
147+
148+
-- Arc is pure lightning, so the player can no longer deal damage with it
149+
assert.are.equals(0, build.calcsTab.calcsOutput.LightningMax)
150+
end)
151+
152+
it("keeps deal no non-elemental damage as its own literal", function()
153+
build.itemsTab:CreateDisplayItemFromRaw([[
154+
New Item
155+
Coral Amulet
156+
Deal no non-Elemental Damage
157+
]])
158+
build.itemsTab:AddDisplayItem()
159+
runCallback("OnFrame")
160+
161+
local modDB = build.calcsTab.mainEnv.player.modDB
162+
assert.truthy(modDB:Flag(nil, "DealNoPhysical"))
163+
assert.truthy(modDB:Flag(nil, "DealNoChaos"))
164+
assert.is_true(not (modDB:Flag(nil, "DealNoLightning")))
165+
assert.is_true(not (modDB:Flag(nil, "DealNoCold")))
166+
assert.is_true(not (modDB:Flag(nil, "DealNoFire")))
167+
end)
168+
169+
it("enemies in your chilling areas take damage increased by the area's chill effect", function()
170+
build.skillsTab:PasteSocketGroup("Slot: Body Armour\nVortex 20/0 1\n")
171+
runCallback("OnFrame")
172+
173+
local baseAvg = build.calcsTab.mainOutput.AverageDamage
174+
assert.is_true(baseAvg > 0)
175+
-- the chill effect currently applied to the enemy (here, from Vortex's chilling area)
176+
local currentChill = build.calcsTab.mainOutput.CurrentChill
177+
assert.is_true(currentChill ~= nil and currentChill > 0)
178+
179+
build.itemsTab:CreateDisplayItemFromRaw([[
180+
New Item
181+
Coral Amulet
182+
Enemies in your Chilling Areas have Cold Damage taken increased by Chill Effect
183+
]])
184+
build.itemsTab:AddDisplayItem()
185+
runCallback("OnFrame")
186+
187+
-- config checkbox not ticked -> enemy is not in the area -> no change
188+
assert.are.equals(baseAvg, build.calcsTab.mainOutput.AverageDamage)
189+
190+
build.configTab.input.conditionEnemyInChillingArea = true
191+
build.configTab:BuildModList()
192+
runCallback("OnFrame")
193+
194+
-- Vortex deals pure cold damage, so the ColdDamageTaken increase scales all of it
195+
local scaledAvg = baseAvg * (1 + currentChill / 100)
196+
assertNearRelative(scaledAvg, build.calcsTab.mainOutput.AverageDamage, 0.005,
197+
string.format("base %.2f scaled by %d%% current chill", baseAvg, currentChill))
198+
199+
-- the paired "Chilled by your Hits" wording must not stack with the chilling area one
200+
build.itemsTab:CreateDisplayItemFromRaw([[
201+
New Item
202+
Coral Ring
203+
Enemies Chilled by your Hits have Cold Damage taken increased by Chill Effect
204+
]])
205+
build.itemsTab:AddDisplayItem()
206+
runCallback("OnFrame")
207+
208+
assertNearRelative(scaledAvg, build.calcsTab.mainOutput.AverageDamage, 0.005,
209+
string.format("both wordings must apply only once (base %.2f, %d%% chill)", baseAvg, currentChill))
210+
end)
211+
212+
-- "Base <ailment> Duration is X seconds" overrides the fixed base duration of the damaging ailments
213+
for _, case in ipairs({
214+
{ ailment = "Ignite", skill = "Fireball", chanceMod = "25% chance to Ignite", gameBase = 4 },
215+
{ ailment = "Bleeding", skill = "Double Strike", chanceMod = "25% chance to cause Bleeding on Hit", gameBase = 5, output = "BleedDuration" },
216+
{ ailment = "Poison", skill = "Double Strike", chanceMod = "25% chance to Poison on Hit", gameBase = 2 },
217+
}) do
218+
it("supports base " .. case.ailment .. " duration override", function()
219+
local outputName = case.output or (case.ailment .. "Duration")
220+
build.itemsTab:CreateDisplayItemFromRaw([[
221+
New Item
222+
Rusted Sword
223+
]])
224+
build.itemsTab:AddDisplayItem()
225+
build.skillsTab:PasteSocketGroup("Slot: Body Armour\n" .. case.skill .. " 20/0 1\n")
226+
build.itemsTab:CreateDisplayItemFromRaw([[
227+
New Item
228+
Coral Amulet
229+
]] .. case.chanceMod)
230+
build.itemsTab:AddDisplayItem()
231+
runCallback("OnFrame")
232+
233+
assert.are.equals(case.gameBase, build.calcsTab.mainOutput[outputName])
234+
235+
build.itemsTab:CreateDisplayItemFromRaw([[
236+
New Item
237+
Coral Ring
238+
Base ]] .. case.ailment .. [[ Duration is 1 second]])
239+
build.itemsTab:AddDisplayItem()
240+
runCallback("OnFrame")
241+
242+
assert.are.equals(1, build.calcsTab.mainOutput[outputName])
243+
end)
244+
end
245+
end)

0 commit comments

Comments
 (0)