Skip to content

Commit 1054f0d

Browse files
author
LocalIdentity
committed
Merge branch 'dev' into new-uniques-0.5
2 parents d385044 + a9c43cb commit 1054f0d

39 files changed

Lines changed: 15652 additions & 8220 deletions

spec/System/TestAttacks_spec.lua

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,113 @@ describe("TestAttacks", function()
167167
assert.are.equals(1.1, build.calcsTab.mainOutput.MainHand.AverageHit)
168168
end)
169169

170+
it("matches in-game tooltip DPS for low-level spear skills", function()
171+
build.spec:SelectClass(build.spec.tree.classNameMap.Huntress)
172+
build.characterLevel = 11
173+
build.characterLevelAutoMode = false
174+
build.controls.characterLevel:SetText(11)
175+
build.configTab.input.customMods = [[
176+
10% increased Attack Damage
177+
+10000 to Accuracy Rating
178+
nearby enemies have 100% less armour
179+
nearby enemies have 100% less evasion
180+
]]
181+
build.configTab:BuildModList()
182+
build.itemsTab:CreateDisplayItemFromRaw([[
183+
Apocalypse Edge
184+
Ironhead Spear
185+
Item Level: 7
186+
Quality: 0
187+
LevelReq: 5
188+
Implicits: 1
189+
Grants Skill: Spear Throw
190+
Adds 2 to 4 Physical Damage
191+
]])
192+
build.itemsTab:AddDisplayItem()
193+
194+
local skills = {
195+
{ gemId = "Metadata/Items/Gems/SkillGemPlayerDefaultSpear", level = 4, dps = 32.8 },
196+
{ gemId = "Metadata/Items/Gems/SkillGemWhirlingSlash", level = 1, dps = 11.8 },
197+
{ gemId = "Metadata/Items/Gems/SkillGemPlayerDefaultSpearThrow", level = 4, dps = 28.8 },
198+
{ gemId = "Metadata/Items/Gems/SkillGemTwister", level = 2, dps = 17.5 },
199+
}
200+
for _, skill in ipairs(skills) do
201+
local group = {
202+
enabled = true,
203+
gemList = { {
204+
gemId = skill.gemId,
205+
level = skill.level,
206+
quality = 0,
207+
enabled = true,
208+
count = 1,
209+
enableGlobal1 = true,
210+
enableGlobal2 = true,
211+
} },
212+
}
213+
table.insert(build.skillsTab.socketGroupList, group)
214+
build.skillsTab:ProcessSocketGroup(group)
215+
skill.groupIndex = #build.skillsTab.socketGroupList
216+
end
217+
218+
for _, skill in ipairs(skills) do
219+
local group = build.skillsTab.socketGroupList[skill.groupIndex]
220+
build.mainSocketGroup = skill.groupIndex
221+
build.calcsTab.input.skill_number = skill.groupIndex
222+
group.mainActiveSkill = 1
223+
group.mainActiveSkillCalcs = 1
224+
build.buildFlag = true
225+
build.modFlag = true
226+
runCallback("OnFrame")
227+
build.calcsTab:BuildOutput()
228+
runCallback("OnFrame")
229+
230+
assert.are.equals(skill.dps, round(build.calcsTab.mainOutput.TotalDPS, 1))
231+
end
232+
end)
233+
234+
it("correctly calculates Garukhan's Resolve bifurcated critical hit damage", function()
235+
local function setup(socketGroup)
236+
newBuild()
237+
build.itemsTab:CreateDisplayItemFromRaw([[
238+
New Item
239+
Razor Quarterstaff
240+
Quality: 0
241+
This Weapon's Critical Hit Chance is 0%
242+
-100% increased physical damage
243+
adds 1 to 1 physical damage to attacks
244+
nearby enemies have 100% less armour
245+
nearby enemies have 100% less evasion
246+
]])
247+
build.itemsTab:AddDisplayItem()
248+
runCallback("OnFrame")
249+
build.skillsTab:PasteSocketGroup(socketGroup)
250+
runCallback("OnFrame")
251+
252+
build.configTab.input.customMods = [[
253+
+50% to critical hit chance
254+
your critical damage bonus is 1000000%
255+
+4000 to accuracy
256+
]]
257+
build.configTab:BuildModList()
258+
runCallback("OnFrame")
259+
build.calcsTab:BuildOutput()
260+
runCallback("OnFrame")
261+
262+
return build.calcsTab.mainOutput.MainHand
263+
end
264+
265+
local normalOutput = setup("Quarterstaff Strike 1/0 1")
266+
assert.are.equals(50, normalOutput.CritChance)
267+
assert.are.equals(10001, normalOutput.CritMultiplier)
268+
assert.are.equals(5001, normalOutput.AverageHit)
269+
270+
local garukhanOutput = setup("Quarterstaff Strike 1/0 1\nGarukhan's Resolve 1/0 1")
271+
assert.are.equals(50, garukhanOutput.PreBifurcateCritChance)
272+
assert.are.equals(75, garukhanOutput.CritChance)
273+
assert.is_true(math.abs(1 / 3 - (garukhanOutput.CritBifurcates - 1)) < 0.000001)
274+
assert.is_true(math.abs(10001 - garukhanOutput.AverageHit) < 0.01)
275+
end)
276+
170277
it("correctly adds damage with oracle forced outcome", function()
171278
-- Setup: Add weapon with no crit chance, and strip enemy defenses
172279
build.itemsTab:CreateDisplayItemFromRaw([[

spec/System/TestDebuffs_spec.lua

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
describe("TestAilments", 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+
it("correctly applies effects dependent on 'Condition:Slowed'", function()
11+
build.skillsTab:PasteSocketGroup("Chaos Bolt 1/0 1\n")
12+
runCallback("OnFrame")
13+
14+
local defaultDmg = build.calcsTab.mainOutput.TotalDPS
15+
assert.True(defaultDmg > 0, "build should deal damage")
16+
17+
build.configTab.input.customMods = "100% increased damage against slowed enemies"
18+
build.configTab:BuildModList()
19+
runCallback("OnFrame")
20+
21+
-- no effect yet
22+
local nonSlowedDmg = build.calcsTab.mainOutput.TotalDPS
23+
assert.are.equals(nonSlowedDmg, defaultDmg, "damage should be unchanged until enemy is slowed")
24+
25+
-- action speed
26+
build.configTab.input.customMods = [[
27+
100% increased damage against slowed enemies
28+
Nearby enemies have 10% reduced action speed
29+
]]
30+
31+
build.configTab:BuildModList()
32+
runCallback("OnFrame")
33+
local actionSlowedDmg = build.calcsTab.mainOutput.TotalDPS
34+
assert.True(actionSlowedDmg > nonSlowedDmg, "damage should be higher vs. reduced action speed")
35+
36+
-- movement speed
37+
build.configTab.input.customMods = [[
38+
100% increased damage against slowed enemies
39+
Nearby enemies have 10% reduced movement speed
40+
]]
41+
42+
build.configTab:BuildModList()
43+
runCallback("OnFrame")
44+
local movementSlowedDmg = build.calcsTab.mainOutput.TotalDPS
45+
assert.True(movementSlowedDmg > nonSlowedDmg, "damage should be higher vs. reduced movement speed")
46+
47+
-- specific slowing debuffs checks
48+
-- NOTE: there might be more conditions that should be checked here, feel free to add more
49+
for _, debuff in ipairs({"chilled", "maimed", "hindered"}) do
50+
build.configTab.input.customMods = [[
51+
100% increased damage against slowed enemies
52+
nearby enemies are ]] .. debuff .. [[
53+
]]
54+
55+
build.configTab:BuildModList()
56+
runCallback("OnFrame")
57+
local debuffSlowedDmg = build.calcsTab.mainOutput.TotalDPS
58+
assert.True(debuffSlowedDmg > nonSlowedDmg, "damage should be higher vs. " .. debuff .. " enemies")
59+
end
60+
61+
-- temporal chains curse
62+
build.configTab.input.customMods = [[
63+
100% increased damage against slowed enemies
64+
]]
65+
build.skillsTab:PasteSocketGroup("Temporal Chains 20/0 1\n")
66+
build.configTab:BuildModList()
67+
runCallback("OnFrame")
68+
local temporalChainsSlowedDmg = build.calcsTab.mainOutput.TotalDPS
69+
assert.True(temporalChainsSlowedDmg > nonSlowedDmg, "damage should be higher with Temporal Chains curse")
70+
end)
71+
end)

spec/System/TestIdolatry_spec.lua

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
describe("TestIdolatry", function()
2+
before_each(function()
3+
newBuild()
4+
end)
5+
6+
-- The Spirit Walker "Idolatry" notable grants three mods that scale with the
7+
-- number of Idols / non-Idol augments (Runes + Soul Cores) socketed across equipped items.
8+
9+
-- Counting: CalcSetup tallies socketed augments by type into the IdolsInEquipment and
10+
-- NonIdolAugmentsInEquipment multipliers, which the three Idolatry mods scale against.
11+
it("counts Idols and non-Idol augments across equipped items", function()
12+
-- Gloves with 2 Idols socketed
13+
build.itemsTab:CreateDisplayItemFromRaw([[
14+
Rarity: MAGIC
15+
Idolatry Test Gloves
16+
Vaal Gloves
17+
Sockets: S S
18+
Rune: Idol of Sirrius
19+
Rune: Idol of Sirrius
20+
Implicits: 0
21+
]])
22+
build.itemsTab:AddDisplayItem()
23+
24+
-- Quarterstaff with 3 Soul Cores socketed (non-Idol augments)
25+
build.itemsTab:CreateDisplayItemFromRaw([[
26+
Rarity: MAGIC
27+
Idolatry Test Staff
28+
Aegis Quarterstaff
29+
Sockets: S S S
30+
Rune: Soul Core of Cholotl
31+
Rune: Soul Core of Zantipi
32+
Rune: Soul Core of Atmohua
33+
Implicits: 0
34+
]])
35+
build.itemsTab:AddDisplayItem()
36+
runCallback("OnFrame")
37+
38+
local modDB = build.calcsTab.mainEnv.modDB
39+
assert.are.equals(2, modDB.multipliers.IdolsInEquipment)
40+
assert.are.equals(3, modDB.multipliers.NonIdolAugmentsInEquipment)
41+
end)
42+
43+
-- Empty sockets (itemSocketCount populated while item.runes has no entry for the slot, e.g. a
44+
-- freshly created base item) must not be counted as augments.
45+
it("does not count empty sockets as augments", function()
46+
build.itemsTab:CreateDisplayItemFromRaw([[
47+
Rarity: MAGIC
48+
Empty Socket Test Gloves
49+
Vaal Gloves
50+
Sockets: S S
51+
Implicits: 0
52+
]])
53+
build.itemsTab:AddDisplayItem()
54+
runCallback("OnFrame")
55+
56+
local modDB = build.calcsTab.mainEnv.modDB
57+
assert.is_nil(modDB.multipliers.IdolsInEquipment)
58+
assert.is_nil(modDB.multipliers.NonIdolAugmentsInEquipment)
59+
end)
60+
61+
-- Parsing: the three stat lines must resolve to mods that scale against those multipliers.
62+
it("parses the three Idolatry stat lines", function()
63+
local parseMod = LoadModule("Modules/ModParser")
64+
65+
-- Helper to find the Multiplier tag on a mod (tags are stored as array entries)
66+
local function multiplierTag(mod)
67+
for _, tag in ipairs(mod) do
68+
if tag.type == "Multiplier" then return tag end
69+
end
70+
end
71+
72+
-- 1) Companion damage scales by the player's Idol count (read via actor = "player"
73+
-- since the mod is evaluated in the companion's own modDB).
74+
local companion = parseMod("Companions deal 10% increased damage per Idol in your Equipment")
75+
assert.are.equals(1, #companion)
76+
assert.are.equals("MinionModifier", companion[1].name)
77+
local inner = companion[1].value.mod
78+
assert.are.equals("Damage", inner.name)
79+
assert.are.equals("INC", inner.type)
80+
assert.are.equals(10, inner.value)
81+
local companionTag = multiplierTag(inner)
82+
assert.is_not_nil(companionTag)
83+
assert.are.equals("IdolsInEquipment", companionTag.var)
84+
assert.are.equals("player", companionTag.actor)
85+
86+
-- 2) Reservation Efficiency scales by the Idol count (player context).
87+
local reservation = parseMod("2% increased Reservation Efficiency of Skills per Idol in your Equipment")
88+
assert.are.equals(1, #reservation)
89+
assert.are.equals("ReservationEfficiency", reservation[1].name)
90+
assert.are.equals("INC", reservation[1].type)
91+
assert.are.equals(2, reservation[1].value)
92+
assert.are.equals("IdolsInEquipment", multiplierTag(reservation[1]).var)
93+
94+
-- 3) Elemental Resistance penalty scales by the non-Idol augment count (player context).
95+
local resist = parseMod("-4% to all Elemental Resistances per non-Idol Augment in your Equipment")
96+
assert.are.equals(1, #resist)
97+
assert.are.equals("ElementalResist", resist[1].name)
98+
assert.are.equals("BASE", resist[1].type)
99+
assert.are.equals(-4, resist[1].value)
100+
assert.are.equals("NonIdolAugmentsInEquipment", multiplierTag(resist[1]).var)
101+
end)
102+
end)

spec/System/TestSkills_spec.lua

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -354,15 +354,27 @@ describe("TestSkills", function()
354354
assert.True(baseLeapSlamHit < build.calcsTab.mainOutput.AverageDamage)
355355
end)
356356

357-
it("applies minion offensive multiplier to all attack damage", function()
357+
it("applies generated minion offensive multiplier to attack damage", function()
358358
build.skillsTab:PasteSocketGroup("Wolf Pack 20/0 1")
359359
runCallback("OnFrame")
360360

361361
local minion = build.calcsTab.mainEnv.minion
362-
local expectedPhysicalMax = round(build.calcsTab.mainEnv.data.monsterAllyDamageTable[minion.level] * (1 + minion.minionData.damageSpread))
362+
local expectedPhysicalMax = floor(floor(build.calcsTab.mainEnv.data.monsterAllyDamageTable[minion.level]) * minion.minionData.damage * (1 + minion.minionData.damageSpread))
363363

364364
assert.are.equals(expectedPhysicalMax, minion.weaponData1.PhysicalMax)
365-
assert.are.near(-30, minion.mainSkill.skillModList:Sum("MORE", minion.mainSkill.skillCfg, "Damage"), 0.0001)
365+
assert.are.near(-30, minion.mainSkill.skillModList:Sum("MORE", minion.mainSkill.skillCfg, "AddedDamage"), 0.0001)
366+
assert.are.equals(0, minion.mainSkill.skillModList:Sum("MORE", minion.mainSkill.skillCfg, "Damage"))
367+
end)
368+
369+
it("does not apply minion offensive multiplier to spectre or companion added damage", function()
370+
for _, skill in ipairs({ "Spectre: Lightless Abomination 20/0 1", "Companion: Lightless Abomination 20/0 1" }) do
371+
newBuild()
372+
build.skillsTab:PasteSocketGroup(skill)
373+
runCallback("OnFrame")
374+
375+
local minion = build.calcsTab.mainEnv.minion
376+
assert.are.equals(0, minion.mainSkill.skillModList:Sum("MORE", minion.mainSkill.skillCfg, "AddedDamage"))
377+
end
366378
end)
367379

368380
it("Inspiring Ally only mirrors companion damage, not generic minion damage", function()
@@ -1106,4 +1118,5 @@ describe("TestSkills", function()
11061118
local noParrySpellDmg = build.calcsTab.mainOutput.AverageDamage
11071119
assert.equals(withParrySpellDmg, noParrySpellDmg, "Parry should not affect spell damage")
11081120
end)
1121+
11091122
end)

src/Classes/CalcBreakdownControl.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ function CalcBreakdownClass:AddModSection(sectionData, modList)
275275
-- Build list of modifiers to display
276276
local cfg = (sectionData.cfg and actor.mainSkill[sectionData.cfg.."Cfg"] and copyTable(actor.mainSkill[sectionData.cfg.."Cfg"], true)) or { }
277277
cfg.source = sectionData.modSource
278-
cfg.ignoreSourceinCheckConditions = true
278+
cfg.ignoreSourceInCheckConditions = true
279279
cfg.actor = sectionData.actor
280280
local rowList
281281
local modStore = (sectionData.enemy and actor.enemy.modDB) or (sectionData.cfg and actor.mainSkill.skillModList) or actor.modDB

src/Classes/CalcSectionControl.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ function CalcSectionClass:FormatStr(str, actor, colData)
190190
local modCfg = (sectionData.cfg and actor.mainSkill[sectionData.cfg.."Cfg"]) or { }
191191
if sectionData.modSource then
192192
modCfg.source = sectionData.modSource
193-
modCfg.ignoreSourceinCheckConditions = true
193+
modCfg.ignoreSourceInCheckConditions = true
194194
end
195195
if sectionData.actor then
196196
modCfg.actor = sectionData.actor

0 commit comments

Comments
 (0)