From b1e93249a6742626f9e6b633e0951c691ef31bcc Mon Sep 17 00:00:00 2001 From: Steve Date: Wed, 22 Jul 2026 01:58:34 +0000 Subject: [PATCH 1/4] Fix Verglas destroyed Ice Crystal damage gain --- spec/System/TestSkills_spec.lua | 64 +++++++++++++++++++++++++++++++++ src/Data/Skills/sup_int.lua | 10 +++++- src/Export/Skills/sup_int.txt | 8 +++++ src/Modules/ConfigOptions.lua | 6 ++++ 4 files changed, 87 insertions(+), 1 deletion(-) diff --git a/spec/System/TestSkills_spec.lua b/spec/System/TestSkills_spec.lua index 54c449879f..0b60a30624 100644 --- a/spec/System/TestSkills_spec.lua +++ b/spec/System/TestSkills_spec.lua @@ -531,6 +531,70 @@ 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.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.input.multiplierDestroyedIceCrystalLife = 2000 + 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]) + assert.is_not_nil(linkedSkill) + assert.is_not_nil(unlinkedSkill) + assert.are.equals(1, linkedSkill.skillModList:Sum("BASE", linkedSkill.skillCfg, "DamageGainAsCold")) + assert.are.equals(0, unlinkedSkill.skillModList:Sum("BASE", unlinkedSkill.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.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") diff --git a/src/Data/Skills/sup_int.lua b/src/Data/Skills/sup_int.lua index ab403cb2dd..16d1c1cb8f 100644 --- a/src/Data/Skills/sup_int.lua +++ b/src/Data/Skills/sup_int.lua @@ -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 = { @@ -8962,4 +8970,4 @@ skills["SupportZenithPlayerTwo"] = { }, }, } -} \ No newline at end of file +} diff --git a/src/Export/Skills/sup_int.txt b/src/Export/Skills/sup_int.txt index 019af6bee6..e782b4af3f 100644 --- a/src/Export/Skills/sup_int.txt +++ b/src/Export/Skills/sup_int.txt @@ -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 diff --git a/src/Modules/ConfigOptions.lua b/src/Modules/ConfigOptions.lua index 0437732526..f6cd2fe637 100644 --- a/src/Modules/ConfigOptions.lua +++ b/src/Modules/ConfigOptions.lua @@ -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 = "Have you Destroyed an Ice Crystal in the past 6 Seconds?", ifCond = "DestroyedIceCrystalPast6Seconds", apply = function(val, modList, enemyModList) + modList:NewMod("Condition:DestroyedIceCrystalPast6Seconds", "FLAG", true, "Config", { type = "Condition", var = "Combat" }) + end }, + { var = "multiplierDestroyedIceCrystalLife", type = "count", label = "Maximum Life of the Destroyed Ice Crystal:", ifOption = "conditionDestroyedIceCrystalPast6Seconds", ifMult = "DestroyedIceCrystalLife", tooltip = "Enter the maximum Life of the Ice Crystal that granted the current Verglas buff.", 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 }, From 42be41e5ec3cc883fa96e1d1b11406324369a4d2 Mon Sep 17 00:00:00 2001 From: Steve Date: Wed, 22 Jul 2026 06:01:13 +0000 Subject: [PATCH 2/4] Auto-calculate Verglas Ice Crystal Life --- spec/System/TestSkills_spec.lua | 32 ++++++++++++++++++++++++++++++-- src/Modules/CalcSetup.lua | 30 ++++++++++++++++++++++++++++++ src/Modules/Calcs.lua | 3 +++ src/Modules/ConfigOptions.lua | 4 ++-- 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/spec/System/TestSkills_spec.lua b/spec/System/TestSkills_spec.lua index 0b60a30624..7ddcd6a6df 100644 --- a/spec/System/TestSkills_spec.lua +++ b/spec/System/TestSkills_spec.lua @@ -536,6 +536,7 @@ describe("TestSkills", function() 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") @@ -544,7 +545,6 @@ describe("TestSkills", function() assert.True(baseFullDPS > 0) build.configTab.input.conditionDestroyedIceCrystalPast6Seconds = true - build.configTab.input.multiplierDestroyedIceCrystalLife = 2000 build.configTab:BuildModList() runCallback("OnFrame") @@ -557,10 +557,25 @@ describe("TestSkills", function() 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.are.equals(1, linkedSkill.skillModList:Sum("BASE", linkedSkill.skillCfg, "DamageGainAsCold")) + 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() @@ -586,6 +601,19 @@ describe("TestSkills", function() 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) + local advancedThaumaturgy = build.spec.nodes[14429] + local qualityOutput = calcFunc({ addNodes = { [advancedThaumaturgy] = true } }, true, { fullDPSOnly = true }) + assert.True(qualityOutput.FullDPS > baseOutput.FullDPS) + build.configTab.input.conditionDestroyedIceCrystalPast6Seconds = false build.configTab:BuildModList() runCallback("OnFrame") diff --git a/src/Modules/CalcSetup.lua b/src/Modules/CalcSetup.lua index 5ba727778c..31c236bada 100644 --- a/src/Modules/CalcSetup.lua +++ b/src/Modules/CalcSetup.lua @@ -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 = { } @@ -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 diff --git a/src/Modules/Calcs.lua b/src/Modules/Calcs.lua index f8cf2e8278..02d29c8248 100644 --- a/src/Modules/Calcs.lua +++ b/src/Modules/Calcs.lua @@ -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) diff --git a/src/Modules/ConfigOptions.lua b/src/Modules/ConfigOptions.lua index f6cd2fe637..57ede0fe7f 100644 --- a/src/Modules/ConfigOptions.lua +++ b/src/Modules/ConfigOptions.lua @@ -1172,10 +1172,10 @@ 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 = "Have you Destroyed an Ice Crystal in the past 6 Seconds?", ifCond = "DestroyedIceCrystalPast6Seconds", apply = function(val, modList, enemyModList) + { var = "conditionDestroyedIceCrystalPast6Seconds", type = "check", label = "Destroyed an Ice Crystal in 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 = "Maximum Life of the Destroyed Ice Crystal:", ifOption = "conditionDestroyedIceCrystalPast6Seconds", ifMult = "DestroyedIceCrystalLife", tooltip = "Enter the maximum Life of the Ice Crystal that granted the current Verglas buff.", apply = function(val, modList, enemyModList) + { 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) From a14c33575eec57e08c2b0ee247721c6f93675e44 Mon Sep 17 00:00:00 2001 From: Steve Date: Wed, 22 Jul 2026 06:04:19 +0000 Subject: [PATCH 3/4] Polish Verglas config labels --- src/Modules/ConfigOptions.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Modules/ConfigOptions.lua b/src/Modules/ConfigOptions.lua index 57ede0fe7f..eb83261ace 100644 --- a/src/Modules/ConfigOptions.lua +++ b/src/Modules/ConfigOptions.lua @@ -1172,7 +1172,7 @@ 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 = "Destroyed an Ice Crystal in past 6s?", ifCond = "DestroyedIceCrystalPast6Seconds", apply = function(val, modList, enemyModList) + { 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) From 507a8180b4cf086c89480aa66f1135c2ffdf7514 Mon Sep 17 00:00:00 2001 From: Steve Date: Wed, 22 Jul 2026 08:01:32 +0000 Subject: [PATCH 4/4] Keep Verglas regression self-contained --- spec/System/TestSkills_spec.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/System/TestSkills_spec.lua b/spec/System/TestSkills_spec.lua index 7ddcd6a6df..413452e7ec 100644 --- a/spec/System/TestSkills_spec.lua +++ b/spec/System/TestSkills_spec.lua @@ -610,9 +610,9 @@ describe("TestSkills", function() assert.are.near(automaticFullDPS, build.calcsTab.mainOutput.FullDPS, automaticFullDPS * 0.001) local calcFunc, baseOutput = LoadModule("Modules/Calcs").getMiscCalculator(build) - local advancedThaumaturgy = build.spec.nodes[14429] - local qualityOutput = calcFunc({ addNodes = { [advancedThaumaturgy] = true } }, true, { fullDPSOnly = true }) - assert.True(qualityOutput.FullDPS > baseOutput.FullDPS) + 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()