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
203 changes: 203 additions & 0 deletions spec/System/TestDeathWish_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
describe("Death Wish minion explosion", function()
local function recalculate(group)
if group then
build.mainSocketGroup = group
build.calcsTab.input.skill_number = group
end
build.buildFlag = true
runCallback("OnFrame")
end

local function minionKey(groupIndex, gemIndex)
local gem = build.skillsTab.socketGroupList[groupIndex].gemList[gemIndex or 1]
return "minion:"..build.skillsTab:GetMinionSourceId(gem)..":"..gem.skillId
end

local function selectMinion(key)
build.configTab.input.deathWishMinion = key
recalculate(2)
end

local function explosionDamage()
return build.calcsTab.mainEnv.player.mainSkill.skillData.FireBonusMin
end

before_each(function()
newBuild()
build.skillsTab:PasteSocketGroup("Summon Stone Golem 20/0 1\nMinion Life 20/0 1")
build.itemsTab:CreateDisplayItemFromRaw([[Maw of Mischief
Bone Helmet
Grants Level 20 Death Wish Skill]])
build.itemsTab:AddDisplayItem()
recalculate(2)
end)

it("defaults to Minion Explosion in the sidebar and Calcs", function()
assert.are.equal(2, build.calcsTab.mainEnv.player.mainSkill.skillPart)
assert.are.equal(2, build.calcsTab.calcsEnv.player.mainSkill.skillPart)
end)

it("uses the summoning skill's calculated life and ignores corpse life", function()
recalculate(1)
local minionLife = build.calcsTab.mainOutput.Minion.Life
assert.is_true(minionLife > 0)
build.configTab.input.detonateDeadCorpseLife = 1000000
build.configTab:BuildModList()
selectMinion(minionKey(1))
assert.are.equal(minionLife, build.calcsTab.mainOutput.DeathWishMinionLife)
assert.are.near(minionLife * 0.19, explosionDamage(), 0.001)
assert.are.equal(minionLife, build.calcsTab.calcsOutput.DeathWishMinionLife)
end)

it("updates when minion gem level, supports and global life scaling change", function()
selectMinion(minionKey(1))
local originalLife = build.calcsTab.mainOutput.DeathWishMinionLife
build.skillsTab.socketGroupList[1].gemList[1].level = 25
build.configTab.input.customMods = "Minions have 100% increased maximum Life"
build.configTab:BuildModList()
recalculate()
local scaledLife = build.calcsTab.mainOutput.DeathWishMinionLife
assert.is_true(scaledLife > originalLife)
recalculate(1)
assert.are.equal(scaledLife, build.calcsTab.mainOutput.Minion.Life)
build.skillsTab.socketGroupList[1].gemList[2].enabled = false
recalculate(2)
assert.is_true(build.calcsTab.mainOutput.DeathWishMinionLife < scaledLife)
end)

it("falls back to enemy corpse life when no minion is selected", function()
build.configTab.input.detonateDeadCorpseLife = 10000
build.configTab:BuildModList()
selectMinion("None")
assert.is_nil(build.calcsTab.mainOutput.DeathWishMinionLife)
assert.are.near(1900, explosionDamage(), 0.001)
end)

it("falls back when the selected minion is disabled or removed", function()
build.configTab.input.detonateDeadCorpseLife = 10000
build.configTab:BuildModList()
selectMinion(minionKey(1))
build.skillsTab.socketGroupList[1].enabled = false
recalculate()
assert.is_nil(build.calcsTab.mainOutput.DeathWishMinionLife)
assert.are.near(1900, explosionDamage(), 0.001)
selectMinion("9:1:SummonStoneGolem")
assert.is_nil(build.calcsTab.mainOutput.DeathWishMinionLife)
end)

it("keeps channelling selectable without explosion damage", function()
selectMinion(minionKey(1))
local gem = build.skillsTab.socketGroupList[2].gemList[1]
gem.skillPart = 1
gem.skillPartCalcs = 1
recalculate()
assert.are.equal(1, build.calcsTab.mainEnv.player.mainSkill.skillPart)
assert.is_nil(explosionDamage())
end)

it("saves and restores the selected minion", function()
selectMinion(minionKey(1))
local life = build.calcsTab.mainOutput.DeathWishMinionLife
local xml = build:SaveDB("code")
loadBuildFromXML(xml)
assert.are.equal(minionKey(1), build.configTab.input.deathWishMinion)
assert.are.equal(life, build.calcsTab.mainOutput.DeathWishMinionLife)
end)

it("distinguishes two sources of the same minion", function()
build.skillsTab:PasteSocketGroup("Summon Stone Golem 1/0 1")
recalculate(3)
local lowLevelLife = build.calcsTab.mainOutput.Minion.Life
selectMinion(minionKey(3))
assert.are.equal(lowLevelLife, build.calcsTab.mainOutput.DeathWishMinionLife)
selectMinion(minionKey(1))
assert.is_true(build.calcsTab.mainOutput.DeathWishMinionLife > lowLevelLife)
end)

it("uses the selected spectre's calculated life", function()
build.spectreList = { "Metadata/Monsters/BloodChieftain/MonkeyChiefBloodEnrage" }
build.skillsTab:PasteSocketGroup("Raise Spectre 20/0 1\nMinion Life 20/0 1")
recalculate(3)
local spectreLife = build.calcsTab.mainOutput.Minion.Life
selectMinion(minionKey(3))
assert.are.equal(spectreLife, build.calcsTab.mainOutput.DeathWishMinionLife)
assert.are.near(spectreLife * 0.19, explosionDamage(), 0.001)
end)

it("uses minion life in Full DPS and calculator comparisons", function()
selectMinion(minionKey(1))
build.skillsTab.socketGroupList[2].includeInFullDPS = true
recalculate()
local selectedDPS = build.calcsTab.mainOutput.FullDPS
assert.is_true(selectedDPS > 0)
local calculator = build.calcsTab.calcs.getMiscCalculator(build)
local result = calculator({ }, false)
assert.are.equal(build.calcsTab.mainOutput.DeathWishMinionLife, result.DeathWishMinionLife)
build.configTab.input.detonateDeadCorpseLife = 1000000
build.configTab:BuildModList()
recalculate()
assert.are.equal(selectedDPS, build.calcsTab.mainOutput.FullDPS)
end)

it("keeps the selected source when groups are reordered and saved", function()
build.skillsTab:PasteSocketGroup("Summon Stone Golem 1/0 1")
selectMinion(minionKey(1))
local key = build.configTab.input.deathWishMinion
local life = build.calcsTab.mainOutput.DeathWishMinionLife
local groups = build.skillsTab.socketGroupList
table.insert(groups, 3, table.remove(groups, 1))
build.skillsTab.controls.groupList:OnOrderChange(1, 3)
recalculate()
assert.are.equal(key, build.configTab.input.deathWishMinion)
assert.are.equal(life, build.calcsTab.mainOutput.DeathWishMinionLife)
loadBuildFromXML(build:SaveDB("code"))
assert.are.equal(key, build.configTab.input.deathWishMinion)
assert.are.equal(life, build.calcsTab.mainOutput.DeathWishMinionLife)
end)

it("keeps the selected source when gems are reordered and undone", function()
selectMinion(minionKey(1))
local key = build.configTab.input.deathWishMinion
local life = build.calcsTab.mainOutput.DeathWishMinionLife
local state = build.skillsTab:CreateUndoState()
local gems = build.skillsTab.socketGroupList[1].gemList
table.insert(gems, table.remove(gems, 1))
recalculate()
assert.are.equal(life, build.calcsTab.mainOutput.DeathWishMinionLife)
build.skillsTab:RestoreUndoState(state)
recalculate()
assert.are.equal(key, build.configTab.input.deathWishMinion)
assert.are.equal(life, build.calcsTab.mainOutput.DeathWishMinionLife)
end)

it("does not select a replacement gem after its source is deleted", function()
selectMinion(minionKey(1))
local key = build.configTab.input.deathWishMinion
table.remove(build.skillsTab.socketGroupList[1].gemList, 1)
recalculate()
assert.is_nil(build.calcsTab.mainOutput.DeathWishMinionLife)
loadBuildFromXML(build:SaveDB("code"))
build.skillsTab:PasteSocketGroup("Summon Stone Golem 20/0 1")
recalculate(2)
assert.are.equal(key, build.configTab.input.deathWishMinion)
assert.is_nil(build.calcsTab.mainOutput.DeathWishMinionLife)
assert.is_true(minionKey(3) ~= key)
end)

it("migrates selections saved with group positions", function()
selectMinion("1:1:SummonStoneGolem")
assert.are.equal(minionKey(1), build.configTab.input.deathWishMinion)
assert.is_true(build.calcsTab.mainOutput.DeathWishMinionLife > 0)
end)

it("populates the selector with enabled minions and a corpse fallback", function()
local control = build.configTab.varControls.deathWishMinion
control.refreshList()
assert.are.equal(2, #control.list)
assert.are.equal("None", control.list[1].val)
assert.are.equal(minionKey(1), control.list[2].val)
control.selFunc(2, control.list[2])
recalculate()
assert.is_true(build.calcsTab.mainOutput.DeathWishMinionLife > 0)
end)
end)
17 changes: 17 additions & 0 deletions src/Classes/ConfigTab.lua
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,15 @@ function ConfigTabClass:ConfigTab(build)
self:BuildModList()
self.build.buildFlag = true
end)
if varData.listFunc then
control.refreshList = function()
control.list = varData.listFunc(self.build)
control:CheckDroppedWidth(true)
control:UpdateSearch()
control.selIndex = 1
control:SelByValue(self.input[varData.var] or self:GetDefaultState(varData.var), "val")
end
end
elseif varData.type == "text" and not varData.resizable then
control = new("EditControl"):EditControl({"TOPLEFT",lastSection,"TOPLEFT"}, {8, 0, 344, 118}, "", nil, "^%C\t\n", nil, function(buf, placeholder)
if placeholder then
Expand Down Expand Up @@ -1015,6 +1024,9 @@ function ConfigTabClass:UpdateControls()
elseif control._className == "CheckBoxControl" then
control.state = self.configSets[self.activeConfigSetId].input[var]
elseif control._className == "DropDownControl" then
if control.refreshList then
control.refreshList()
end
control:SelByValue(self.configSets[self.activeConfigSetId].input[var] or self:GetDefaultState(var), "val")
end
end
Expand All @@ -1041,6 +1053,11 @@ function ConfigTabClass:Draw(viewPort, inputEvents)
end
end

for _, control in pairs(self.varControls) do
if control.refreshList then
control.refreshList()
end
end
self:ProcessControlsInput(inputEvents, viewPort)
for _, event in ipairs(inputEvents) do
if event.type == "KeyUp" then
Expand Down
18 changes: 18 additions & 0 deletions src/Classes/SkillsTab.lua
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ function SkillsTabClass:SkillsTab(build)
self:Control()

self.build = build
self.nextMinionSourceId = 1

self.socketGroupList = { }

Expand Down Expand Up @@ -460,6 +461,8 @@ function SkillsTabClass:LoadSkill(node, skillSetId)

for _, child in ipairs(node) do
local gemInstance = { }
gemInstance.minionSourceId = tonumber(child.attrib.minionSourceId)
self.nextMinionSourceId = m_max(self.nextMinionSourceId, (gemInstance.minionSourceId or 0) + 1)
gemInstance.nameSpec = sanitiseText(child.attrib.nameSpec or "")
if child.attrib.gemId then
local gemData
Expand Down Expand Up @@ -521,6 +524,7 @@ function SkillsTabClass:LoadSkill(node, skillSetId)
end

function SkillsTabClass:Load(xml, fileName)
self.nextMinionSourceId = tonumber(xml.attrib.nextMinionSourceId) or 1
self.activeSkillSetId = 0
self.skillSets = { }
self.skillSetOrderList = { }
Expand Down Expand Up @@ -572,6 +576,7 @@ end

function SkillsTabClass:Save(xml)
xml.attrib = {
nextMinionSourceId = tostring(self.nextMinionSourceId),
activeSkillSet = tostring(self.activeSkillSetId),
defaultGemLevel = self.defaultGemLevel,
defaultGemQuality = tostring(self.defaultGemQuality),
Expand Down Expand Up @@ -599,6 +604,7 @@ function SkillsTabClass:Save(xml)
} }
for _, gemInstance in ipairs(socketGroup.gemList) do
t_insert(node, { elem = "Gem", attrib = {
minionSourceId = gemInstance.minionSourceId and tostring(gemInstance.minionSourceId),
nameSpec = gemInstance.nameSpec,
skillId = gemInstance.skillId,
gemId = gemInstance.gemData and gemInstance.gemData.gameId,
Expand Down Expand Up @@ -1156,6 +1162,15 @@ function SkillsTabClass:ProcessGemLevel(gemData, imbued)
end
end

-- Allocate once and preserve through reordering, undo and build saves.
function SkillsTabClass:GetMinionSourceId(gemInstance)
if not gemInstance.minionSourceId then
gemInstance.minionSourceId = self.nextMinionSourceId
self.nextMinionSourceId = self.nextMinionSourceId + 1
end
return gemInstance.minionSourceId
end

-- Processes the given socket group, filling in information that will be used for display or calculations
---@param socketGroup table
function SkillsTabClass:ProcessSocketGroup(socketGroup)
Expand Down Expand Up @@ -1209,6 +1224,9 @@ function SkillsTabClass:ProcessSocketGroup(socketGroup)
if gemInstance.gemData or gemInstance.grantedEffect then
gemInstance.new = nil
local grantedEffect = gemInstance.grantedEffect or gemInstance.gemData.grantedEffect
if grantedEffect.skillTypes and grantedEffect.skillTypes[SkillType.Minion] then
self:GetMinionSourceId(gemInstance)
end
if grantedEffect.color == 1 then
gemInstance.color = colorCodes.STRENGTH
elseif grantedEffect.color == 2 then
Expand Down
8 changes: 1 addition & 7 deletions src/Data/Skills/other.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1266,6 +1266,7 @@ skills["DeathWish"] = {
statDescriptionScope = "skill_stat_descriptions",
castTime = 0.2,
fromItem = true,
defaultPart = 2,
parts = {
{
name = "Channelling",
Expand All @@ -1279,13 +1280,6 @@ skills["DeathWish"] = {
stages = true,
},
},
preDamageFunc = function(activeSkill, output)
if activeSkill.skillPart == 2 then
local skillData = activeSkill.skillData
skillData.FireBonusMin = output.Life * skillData.selfFireExplosionLifeMultiplier
skillData.FireBonusMax = output.Life * skillData.selfFireExplosionLifeMultiplier
end
end,
statMap = {
["spell_minimum_base_fire_damage"] = {
skill("FireMin", nil, { type = "SkillPart", skillPart = 2 }),
Expand Down
8 changes: 1 addition & 7 deletions src/Export/Skills/other.txt
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ return function(skills, mod, flag, skill)
#skill DeathWish
#flags spell area
fromItem = true,
defaultPart = 2,
parts = {
{
name = "Channelling",
Expand All @@ -414,13 +415,6 @@ return function(skills, mod, flag, skill)
stages = true,
},
},
preDamageFunc = function(activeSkill, output)
if activeSkill.skillPart == 2 then
local skillData = activeSkill.skillData
skillData.FireBonusMin = output.Life * skillData.selfFireExplosionLifeMultiplier
skillData.FireBonusMax = output.Life * skillData.selfFireExplosionLifeMultiplier
end
end,
statMap = {
["spell_minimum_base_fire_damage"] = {
skill("FireMin", nil, { type = "SkillPart", skillPart = 2 }),
Expand Down
23 changes: 21 additions & 2 deletions src/Modules/CalcActiveSkill.lua
Original file line number Diff line number Diff line change
Expand Up @@ -248,10 +248,10 @@ function calcs.buildActiveSkillModList(env, activeSkill)
local activeGemParts = activeGrantedEffect.parts
if activeGemParts and #activeGemParts > 1 then
if env.mode == "CALCS" and activeSkill == env.player.mainSkill then
activeEffect.srcInstance.skillPartCalcs = m_min(#activeGemParts, activeEffect.srcInstance.skillPartCalcs or 1)
activeEffect.srcInstance.skillPartCalcs = m_min(#activeGemParts, activeEffect.srcInstance.skillPartCalcs or activeGrantedEffect.defaultPart or 1)
activeSkill.skillPart = activeEffect.srcInstance.skillPartCalcs
else
activeEffect.srcInstance.skillPart = m_min(#activeGemParts, activeEffect.srcInstance.skillPart or 1)
activeEffect.srcInstance.skillPart = m_min(#activeGemParts, activeEffect.srcInstance.skillPart or activeGrantedEffect.defaultPart or 1)
activeSkill.skillPart = activeEffect.srcInstance.skillPart
end
local part = activeGemParts[activeSkill.skillPart]
Expand Down Expand Up @@ -932,3 +932,22 @@ function calcs.createMinionSkills(env, activeSkill)
end
minion.mainSkill = minion.activeSkillList[skillIndex]
end

-- Keep duplicate summoning gems distinct, even when they summon the same minion.
function calcs.minionSourceKey(activeSkill, skillsTab)
if not activeSkill.minion or activeSkill.minion.hostile or activeSkill.skillFlags.disable or activeSkill.skillTypes[SkillType.MinionsAreUndamagable] then
return
end
for groupIndex, group in ipairs(skillsTab.socketGroupList) do
if group == activeSkill.socketGroup then
for gemIndex, gem in ipairs(group.gemList) do
if gem == activeSkill.activeEffect.srcInstance then
local effectId = activeSkill.activeEffect.grantedEffect.id
local key = "minion:"..skillsTab:GetMinionSourceId(gem)..":"..effectId
local legacyKey = groupIndex..":"..gemIndex..":"..effectId
return key, legacyKey
end
end
end
end
end
4 changes: 2 additions & 2 deletions src/Modules/CalcOffence.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1913,8 +1913,8 @@ function calcs.offence(env, actor, activeSkill)
end

-- Handle corpse and enemy explosions
local monsterLife = skillData.corpseLife or (env.enemyLevel and data.monsterLifeTable[env.enemyLevel] or 100)
if skillData.explodeCorpse and (skillData.corpseLife or env.enemyLevel) then
local monsterLife = output.DeathWishMinionLife or skillData.corpseLife or (env.enemyLevel and data.monsterLifeTable[env.enemyLevel] or 100)
if skillData.explodeCorpse and (output.DeathWishMinionLife or skillData.corpseLife or env.enemyLevel) then
local damageType = skillData.corpseExplosionDamageType or "Fire"
skillData[damageType.."BonusMin"] = monsterLife * ( skillData.corpseExplosionLifeMultiplier or skillData.selfFireExplosionLifeMultiplier )
skillData[damageType.."BonusMax"] = monsterLife * ( skillData.corpseExplosionLifeMultiplier or skillData.selfFireExplosionLifeMultiplier )
Expand Down
Loading
Loading