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
92 changes: 92 additions & 0 deletions spec/System/TestSkills_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,98 @@ describe("TestSkills", function()
assert.True(math.abs(build.calcsTab.mainOutput.FullDPS - fullDPS * 0.5) < fullDPS * 0.001)
end)

it("applies Verglas from destroyed Ice Crystal Life only to supported skills", function()
build.itemsTab:CreateDisplayItemFromRaw("New Item\nRazor Quarterstaff\nQuality: 0")
build.itemsTab:AddDisplayItem()
build.skillsTab:PasteSocketGroup("Quarterstaff Strike 20/0 1\nVerglas 1/0 1")
build.skillsTab:PasteSocketGroup("Leap Slam 20/0 1")
build.skillsTab:PasteSocketGroup("Frost Wall 20/20 1")
build.skillsTab.socketGroupList[1].includeInFullDPS = true
runCallback("OnFrame")

local baseFullDPS = build.calcsTab.mainOutput.FullDPS
assert.truthy(baseFullDPS)
assert.True(baseFullDPS > 0)

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

local function findSkillForGroup(socketGroup)
for _, activeSkill in ipairs(build.calcsTab.mainEnv.player.activeSkillList) do
if activeSkill.socketGroup == socketGroup then
return activeSkill
end
end
end
local linkedSkill = findSkillForGroup(build.skillsTab.socketGroupList[1])
local unlinkedSkill = findSkillForGroup(build.skillsTab.socketGroupList[2])
local frostWallSkill = findSkillForGroup(build.skillsTab.socketGroupList[3])
assert.is_not_nil(linkedSkill)
assert.is_not_nil(unlinkedSkill)
assert.is_not_nil(frostWallSkill)
local frostWallLife = frostWallSkill.skillModList:Sum("BASE", frostWallSkill.skillCfg, "IceCrystalLifeBase") * calcLib.mod(frostWallSkill.skillModList, frostWallSkill.skillCfg, "IceCrystalLife")
assert.are.equals(frostWallLife, build.calcsTab.mainEnv.player.automaticDestroyedIceCrystalLife)
assert.are.equals(frostWallLife, build.calcsTab.mainEnv.player.destroyedIceCrystalLife)
assert.are.equals(tostring(math.floor(frostWallLife + 0.5)), build.configTab.varControls.multiplierDestroyedIceCrystalLife.placeholder)
assert.are.equals(math.floor(frostWallLife / 2000), linkedSkill.skillModList:Sum("BASE", linkedSkill.skillCfg, "DamageGainAsCold"))
assert.are.equals(0, unlinkedSkill.skillModList:Sum("BASE", unlinkedSkill.skillCfg, "DamageGainAsCold"))
local automaticFullDPS = build.calcsTab.mainOutput.FullDPS
assert.True(automaticFullDPS > baseFullDPS)

build.configTab.input.multiplierDestroyedIceCrystalLife = 2000
build.configTab:BuildModList()
runCallback("OnFrame")

linkedSkill = findSkillForGroup(build.skillsTab.socketGroupList[1])
assert.are.equals(1, linkedSkill.skillModList:Sum("BASE", linkedSkill.skillCfg, "DamageGainAsCold"))

build.configTab.input.multiplierDestroyedIceCrystalLife = 3999
build.configTab:BuildModList()
runCallback("OnFrame")

linkedSkill = findSkillForGroup(build.skillsTab.socketGroupList[1])
assert.are.equals(1, linkedSkill.skillModList:Sum("BASE", linkedSkill.skillCfg, "DamageGainAsCold"))
local fullDPSAt3999Life = build.calcsTab.mainOutput.FullDPS
assert.True(fullDPSAt3999Life > baseFullDPS)

build.configTab.input.multiplierDestroyedIceCrystalLife = 4000
build.configTab:BuildModList()
runCallback("OnFrame")

linkedSkill = findSkillForGroup(build.skillsTab.socketGroupList[1])
assert.are.equals(2, linkedSkill.skillModList:Sum("BASE", linkedSkill.skillCfg, "DamageGainAsCold"))
assert.True(build.calcsTab.mainOutput.FullDPS > fullDPSAt3999Life)

build.configTab.input.multiplierDestroyedIceCrystalLife = 271990
build.configTab:BuildModList()
runCallback("OnFrame")

linkedSkill = findSkillForGroup(build.skillsTab.socketGroupList[1])
assert.are.equals(135, linkedSkill.skillModList:Sum("BASE", linkedSkill.skillCfg, "DamageGainAsCold"))

build.configTab.input.multiplierDestroyedIceCrystalLife = nil
build.configTab:BuildModList()
runCallback("OnFrame")

linkedSkill = findSkillForGroup(build.skillsTab.socketGroupList[1])
assert.are.equals(math.floor(frostWallLife / 2000), linkedSkill.skillModList:Sum("BASE", linkedSkill.skillCfg, "DamageGainAsCold"))
assert.are.near(automaticFullDPS, build.calcsTab.mainOutput.FullDPS, automaticFullDPS * 0.001)

local calcFunc, baseOutput = LoadModule("Modules/Calcs").getMiscCalculator(build)
build.skillsTab:PasteSocketGroup("Frost Wall 20/20 1\nGlacier 1/0 1")
local glacierOutput = calcFunc(nil, true, { fullDPSOnly = true })
assert.True(glacierOutput.FullDPS > baseOutput.FullDPS)

build.configTab.input.conditionDestroyedIceCrystalPast6Seconds = false
build.configTab:BuildModList()
runCallback("OnFrame")

linkedSkill = findSkillForGroup(build.skillsTab.socketGroupList[1])
assert.are.equals(0, linkedSkill.skillModList:Sum("BASE", linkedSkill.skillCfg, "DamageGainAsCold"))
assert.are.near(baseFullDPS, build.calcsTab.mainOutput.FullDPS, baseFullDPS * 0.001)
end)

it("Test mana cost efficiency with support gems", function()
-- Test interaction between cost efficiency and cost multipliers
build.skillsTab:PasteSocketGroup("Contagion 6/0 1\nMagnified Area I 1/0 1")
Expand Down
10 changes: 9 additions & 1 deletion src/Data/Skills/sup_int.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8553,6 +8553,14 @@ skills["SupportVerglasPlayer"] = {
label = "Verglas",
incrementalEffectiveness = 0.054999999701977,
statDescriptionScope = "gem_stat_descriptions",
statMap = {
["support_crystalshatter_buff_damage_%_gained_as_extra_cold_per_2000_crystal_life"] = {
mod("DamageGainAsCold", "BASE", nil, 0, 0, { type = "Condition", var = "DestroyedIceCrystalPast6Seconds" }, { type = "Multiplier", var = "DestroyedIceCrystalLife", div = 2000 }),
},
["support_crystalshatter_buff_duration"] = {
-- Display only
},
},
baseFlags = {
},
constantStats = {
Expand Down Expand Up @@ -8962,4 +8970,4 @@ skills["SupportZenithPlayerTwo"] = {
},
},
}
}
}
8 changes: 8 additions & 0 deletions src/Export/Skills/sup_int.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1521,6 +1521,14 @@ statMap = {

#skill SupportVerglasPlayer
#set SupportVerglasPlayer
statMap = {
["support_crystalshatter_buff_damage_%_gained_as_extra_cold_per_2000_crystal_life"] = {
mod("DamageGainAsCold", "BASE", nil, 0, 0, { type = "Condition", var = "DestroyedIceCrystalPast6Seconds" }, { type = "Multiplier", var = "DestroyedIceCrystalLife", div = 2000 }),
},
["support_crystalshatter_buff_duration"] = {
-- Display only
},
},
#mods
#skillEnd

Expand Down
30 changes: 30 additions & 0 deletions src/Modules/CalcSetup.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ local t_insert = table.insert
local t_remove = table.remove
local m_min = math.min
local m_max = math.max
local m_floor = math.floor
local band = AND64

local tempTable1 = { }
Expand Down Expand Up @@ -2093,6 +2094,35 @@ function calcs.initEnv(build, mode, override, specEnv)
end
end

-- Verglas uses the maximum Life of the Ice Crystal that granted its buff.
-- Prefer the highest value from enabled Ice Crystal skills in the build, while
-- retaining the Config value as an explicit override for other crystal sources.
local automaticDestroyedIceCrystalLife = 0
for _, activeSkill in pairs(env.player.activeSkillList) do
local socketGroup = activeSkill.socketGroup
if (not socketGroup or (socketGroup.enabled and socketGroup.slotEnabled)) and activeSkill.skillModList then
local baseLife = activeSkill.skillModList:Sum("BASE", activeSkill.skillCfg, "IceCrystalLifeBase")
if baseLife > 0 then
automaticDestroyedIceCrystalLife = m_max(automaticDestroyedIceCrystalLife, baseLife * calcLib.mod(activeSkill.skillModList, activeSkill.skillCfg, "IceCrystalLife"))
end
end
end
local configuredDestroyedIceCrystalLife = tonumber(env.configInput.multiplierDestroyedIceCrystalLife) or 0
local resolvedDestroyedIceCrystalLife = configuredDestroyedIceCrystalLife > 0 and configuredDestroyedIceCrystalLife or automaticDestroyedIceCrystalLife
env.player.automaticDestroyedIceCrystalLife = automaticDestroyedIceCrystalLife
env.player.destroyedIceCrystalLife = resolvedDestroyedIceCrystalLife
if configuredDestroyedIceCrystalLife <= 0 and automaticDestroyedIceCrystalLife > 0 then
env.modDB.multipliers.DestroyedIceCrystalLife = automaticDestroyedIceCrystalLife
else
env.modDB.multipliers.DestroyedIceCrystalLife = nil
end
if mode == "MAIN" then
local control = build.configTab.varControls.multiplierDestroyedIceCrystalLife
if control then
control:SetPlaceholder(automaticDestroyedIceCrystalLife > 0 and m_floor(automaticDestroyedIceCrystalLife + 0.5) or "", false)
end
end

env.virtuousMoteSkillCount = virtuousMoteSkillCount
env.modDB.multipliers.StrengthMoteSkillCount = virtuousMoteSkillCount.Str
env.modDB.multipliers.DexterityMoteSkillCount = virtuousMoteSkillCount.Dex
Expand Down
3 changes: 3 additions & 0 deletions src/Modules/Calcs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@ end
local exposureElements = { "Fire", "Cold", "Lightning", "Chaos" }
local function captureCouplingSurface(env)
local surface = { mods = { }, meta = { } }
-- Automatic Verglas scaling is derived from another active skill (usually
-- Frost Wall), so it must participate in Full-DPS cache invalidation.
surface.meta[#surface.meta + 1] = "destroyedIceCrystalLife/" .. tostring(env.player.destroyedIceCrystalLife or 0)
for _, skill in ipairs(env.player.activeSkillList) do
for _, buff in ipairs(skill.buffList or { }) do
surface.meta[#surface.meta + 1] = tostring(buff.type) .. "/" .. tostring(buff.name)
Expand Down
6 changes: 6 additions & 0 deletions src/Modules/ConfigOptions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1172,6 +1172,12 @@ Huge sets the radius to 11.
{ var = "conditionKilledLast3Seconds", type = "check", label = "Have you Killed in the last 3 Seconds?", ifCond = "KilledLast3Seconds", implyCond = "KilledRecently", tooltip = "This also implies that you have Killed Recently.", apply = function(val, modList, enemyModList)
modList:NewMod("Condition:KilledLast3Seconds", "FLAG", true, "Config", { type = "Condition", var = "Combat" })
end },
{ var = "conditionDestroyedIceCrystalPast6Seconds", type = "check", label = "Ice Crystal destroyed (past 6s)?", ifCond = "DestroyedIceCrystalPast6Seconds", apply = function(val, modList, enemyModList)
modList:NewMod("Condition:DestroyedIceCrystalPast6Seconds", "FLAG", true, "Config", { type = "Condition", var = "Combat" })
end },
{ var = "multiplierDestroyedIceCrystalLife", type = "count", label = "Ice Crystal Life override:", ifOption = "conditionDestroyedIceCrystalPast6Seconds", ifMult = "DestroyedIceCrystalLife", tooltip = "Automatically uses the highest maximum Life from enabled Ice Crystal skills.\nEnter a value to override it for another Ice Crystal source.", apply = function(val, modList, enemyModList)
modList:NewMod("Multiplier:DestroyedIceCrystalLife", "BASE", val, "Config", { type = "Condition", var = "Combat" })
end },
{ var = "conditionKilledPoisonedLast2Seconds", type = "check", label = "Killed a poisoned enemy in the last 2 Seconds?", ifCond = "KilledPoisonedLast2Seconds", implyCond = "KilledRecently", tooltip = "This also implies that you have Killed Recently.", apply = function(val, modList, enemyModList)
modList:NewMod("Condition:KilledPoisonedLast2Seconds", "FLAG", true, "Config", { type = "Condition", var = "Combat" })
end },
Expand Down