Skip to content
Closed
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
52 changes: 52 additions & 0 deletions spec/System/TestAilments_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,56 @@ describe("TestAilments", function()

assert.are.equals(1.1, build.calcsTab.mainOutput.PoisonEffMult)
end)

-- Pseudo-hits: a skill flagged with skillData.pseudoHitAilment inflicts that one
-- ailment as though dealing its hit damage, but the hit itself deals no damage
-- (e.g. Infernal Legion's burn). No in-tree skill sets the flag yet, so run the
-- calcs directly on an env with the flag injected.
describe("pseudo-hit ailment sources", function()
local function setupFireball(customMods)
build.skillsTab:PasteSocketGroup("Fireball 20/0 1\n")
runCallback("OnFrame")
build.configTab.input.customMods = customMods
build.configTab:BuildModList()
runCallback("OnFrame")
end

local function performWith(pseudoHitAilment)
local calcs = build.calcsTab.calcs
local env = calcs.initEnv(build, "MAIN")
env.player.mainSkill.skillData.pseudoHitAilment = pseudoHitAilment
calcs.perform(env)
return env.player.output
end

it("deals no hit damage but still applies its ailment", function()
setupFireball("100% chance to Ignite")
local real = performWith(nil)
assert.True(real.TotalDPS and real.TotalDPS > 0)
assert.True(real.IgniteDPS and real.IgniteDPS > 0)
local pseudo = performWith("Ignite")
assert.are.equals(0, pseudo.TotalDPS)
assert.are.equals(0, pseudo.AverageDamage)
-- the ailment still sees the full stored hit damage
assert.are.equals(real.IgniteDPS, pseudo.IgniteDPS)
end)

it("does not let other on-hit ailments ride on the notional damage", function()
setupFireball("100% chance to Ignite\n100% chance to Poison on Hit\nAdds 50 to 70 Chaos Damage to Spells")
local real = performWith(nil)
assert.True(real.PoisonDPS and real.PoisonDPS > 0)
local pseudo = performWith("Ignite")
assert.True(not pseudo.PoisonDPS or pseudo.PoisonDPS == 0, "pseudo-hit must not produce poison DPS")
assert.are.equals(real.IgniteDPS, pseudo.IgniteDPS)
end)

it("still scales with ailment magnitude", function()
setupFireball("100% chance to Ignite")
local base = performWith("Ignite").IgniteDPS
assert.True(base and base > 0)
setupFireball("100% chance to Ignite\n50% increased Magnitude of Ailments")
local scaled = performWith("Ignite").IgniteDPS
assert.True(math.abs(scaled / base - 1.5) < 1e-6, string.format("expected 1.5x, got %.6fx", scaled / base))
end)
end)
end)
122 changes: 122 additions & 0 deletions spec/System/TestInfernalLegion_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
describe("TestInfernalLegion", function()
before_each(function()
newBuild()
end)

-- Helper: minion supported by Infernal Legion, with the minion pointed at the IL skill.
-- Set any config/customMods before calling this so the build picks them up.
local function setupIL()
build.configTab:BuildModList()
build.skillsTab:PasteSocketGroup("Skeletal Warrior 20/0 1\nInfernal Legion I 1/0 1\n")
runCallback("OnFrame")
build.skillsTab:SetDisplayGroup(build.skillsTab.socketGroupList[1])
runCallback("OnFrame")
local env = build.calcsTab.mainEnv
local ilIndex
for i, s in ipairs(env.minion and env.minion.activeSkillList or {}) do
local ge = s.activeEffect and s.activeEffect.grantedEffect
if ge and ge.id == "InfernalLegion" then ilIndex = i break end
end
assert.is_not_nil(ilIndex, "Infernal Legion should be on the minion's skill list")
for _, gem in ipairs(build.skillsTab.socketGroupList[1].gemList) do
gem.skillMinionSkill = ilIndex
gem.skillMinionSkillCalcs = ilIndex
end
build.mainSocketGroup = 1
build.modFlag = true; build.buildFlag = true
for _ = 1, 6 do runCallback("OnFrame") end
return build.calcsTab.mainEnv
end

local function recalc()
build.configTab:BuildModList()
build.modFlag = true; build.buildFlag = true
for _ = 1, 4 do runCallback("OnFrame") end
return build.calcsTab.mainEnv
end

-- IL's "hit" is a pseudo-hit: it seeds the ignite but deals no damage of its own.
it("IL deals no hit damage (pseudo-hit) but still ignites", function()
local o = setupIL().minion.output
assert.are.equals(0, o.AverageDamage)
assert.are.equals(0, o.TotalDPS)
assert.True(o.IgniteDPS and o.IgniteDPS > 0, "IL should produce ignite DPS")
end)

-- IL ignite crits use a 50% base crit bonus (1.5x), not the minion's default 100% (2x).
it("IL uses a 50% base crit bonus (1.5x multiplier)", function()
assert.are.equals(1.5, setupIL().minion.output.PreEffectiveCritMultiplier)
end)

-- The minion's *increased* crit damage applies at half effectiveness for IL.
it("the minion's increased crit damage is half-effective for IL", function()
build.configTab.input.customMods = "Minions have 100% increased Critical Damage Bonus"
-- base 0.5 bonus, +100% increased applied at half (=+50%): 0.5 * (1 + 0.5) = 0.75 -> 1.75x.
-- (Full effectiveness would give 0.5 * 2.0 = 1.0 -> 2.0x.)
assert.are.equals(1.75, setupIL().minion.output.PreEffectiveCritMultiplier)
end)

-- "Minions have X% increased Magnitude of Ailments" scales IL's ignite end-to-end.
it("minion 'increased Magnitude of Ailments' scales IL ignite", function()
local base = setupIL().minion.output.IgniteDPS
assert.True(base and base > 0)
build.configTab.input.customMods = "Minions have 100% increased Magnitude of Ailments"
local scaled = recalc().minion.output.IgniteDPS
-- +100% magnitude roughly doubles the ignite (baseline magnitude increase is 0 here).
assert.True(math.abs(scaled - 2 * base) < 1e-6, string.format("expected %.6f, got %.6f", 2 * base, scaled))
end)

-- Full DPS runs offence on the IL skill an extra time. The half-effect that
-- preDamageFunc injects must not stack, or crit would over-halve.
it("IL crit/ignite stay correct under Full DPS (no double-applied halving)", function()
local base = setupIL().minion.output.IgniteDPS
build.skillsTab.socketGroupList[1].includeInFullDPS = true
local env = recalc()
assert.are.equals(1.5, env.minion.output.PreEffectiveCritMultiplier)
assert.True(math.abs(env.minion.output.IgniteDPS - base) < 1e-6,
string.format("ignite changed under Full DPS: %.6f vs %.6f", env.minion.output.IgniteDPS, base))
end)

-- The IL extra-skill result must live in the pass snapshot: a cached Full DPS
-- replay (item/tree comparison) has to keep the IL ignite, not silently drop it.
it("IL ignite survives a cached Full DPS replay", function()
local env = setupIL()
-- select a non-IL skill as the minion's main skill; IL stays granted as the
-- extra skill, so its ignite only enters Full DPS via the extra-skill pass
local otherIndex
for i, s in ipairs(env.minion.activeSkillList) do
local ge = s.activeEffect and s.activeEffect.grantedEffect
if not (ge and ge.id == "InfernalLegion") then otherIndex = i break end
end
assert.is_not_nil(otherIndex, "no non-IL minion skill found")
for _, gem in ipairs(build.skillsTab.socketGroupList[1].gemList) do
gem.skillMinionSkill = otherIndex
gem.skillMinionSkillCalcs = otherIndex
end
build.skillsTab.socketGroupList[1].includeInFullDPS = true
recalc()
local calcsModule = LoadModule("Modules/Calcs")
local store = { }
local fresh = calcsModule.calcFullDPS(build, "CALCULATOR", {}, { fullDPSCache = { store = store, capture = true } })
local performs = 0
local realPerform = calcsModule.perform
calcsModule.perform = function(...) performs = performs + 1 return realPerform(...) end
local cached = calcsModule.calcFullDPS(build, "CALCULATOR", {}, { fullDPSCache = { store = store } })
calcsModule.perform = realPerform
assert.are.equals(0, performs, "second call should replay from the cache")
assert.True(fresh.igniteDPS and fresh.igniteDPS > 0, "fresh pass should have the IL ignite")
assert.are.equals(fresh.igniteDPS, cached.igniteDPS)
assert.are.equals(fresh.TotalDotDPS, cached.TotalDotDPS)
end)

-- IL's hit is a pseudo-hit (no real damage), so a poison source must not seed a
-- poison off its big notional damage -- that was inflating Poison's evaluated value.
it("IL pseudo-hit does not seed poison from a poison source", function()
local igniteBefore = setupIL().minion.output.IgniteDPS
build.configTab.input.customMods = "Minions have 100% chance to Poison on Hit"
local env = recalc()
local poison = env.minion.output.PoisonDPS
assert.True(not poison or poison == 0, "IL pseudo-hit must not produce poison DPS")
assert.True(math.abs(env.minion.output.IgniteDPS - igniteBefore) < 1e-6, "ignite must be unchanged")
end)
end)
3 changes: 3 additions & 0 deletions src/Data/SkillStatMap.lua
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,9 @@ return {
["active_skill_base_radius_+"] = {
skill("radiusExtra", nil),
},
["infernal_legion_minion_burning_effect_radius"] = {
skill("radius", nil),
},
["base_skill_area_of_effect_+%"] = {
mod("AreaOfEffect", "INC", nil),
},
Expand Down
52 changes: 52 additions & 0 deletions src/Data/Skills/minion.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1132,6 +1132,58 @@ skills["MPSAncestralTotemSpiritSoulCasterProjectile"] = {
}
}

skills["InfernalLegion"] = {
name = "Infernal Legion",
hidden = true,
skillTypes = {
[SkillType.Damage] = true,
[SkillType.Area] = true,
[SkillType.Fire] = true,
[SkillType.CausesBurning] = true,
},
qualityStats = {
},
levels = {
[1] = { critChance = 0, levelRequirement = 0, }, -- only crits from enemy Critical Weakness
},
preDamageFunc = function(activeSkill, output)
local skillData = activeSkill.skillData
local sml = activeSkill.skillModList
local cfg = activeSkill.skillCfg
local base = output.Life * skillData.selfFireExplosionLifeMultiplier
skillData.FireBonusMin = base
skillData.FireBonusMax = base
-- minion's increased crit chance/damage are half-effective for IL
sml:NewMod("CritChance", "INC", -0.5 * sml:Sum("INC", cfg, "CritChance"), "Infernal Legion")
sml:NewMod("CritMultiplier", "INC", -0.5 * sml:Sum("INC", cfg, "CritMultiplier"), "Infernal Legion")
end,
statSets = {
[1] = {
label = "Infernal Legion",
incrementalEffectiveness = 0,
statDescriptionScope = "skill_stat_descriptions",
baseFlags = {
area = true,
fire = true,
},
baseMods = {
skill("selfFireExplosionLifeMultiplier", 0.01, { type = "Multiplier", var = "InfernalLegionBaseDamage" }),
skill("pseudoHitAilment", "Ignite"), -- fake hit: only seeds Ignite, no hit damage
skill("timeOverride", 1.25), -- fixed ~0.8/sec ignite cadence, not attack speed
mod("EnemyIgniteChance", "BASE", 100),
mod("CritMultiplier", "BASE", -50), -- 50% base crit bonus, not the minion's 100%
},
constantStats = {
},
stats = {
},
levels = {
[1] = { },
},
},
}
}

skills["GAAnimateWeaponMaceSlam"] = {
name = "Mace Slam",
hidden = true,
Expand Down
9 changes: 9 additions & 0 deletions src/Data/Skills/other.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3556,6 +3556,15 @@ skills["SupportCathasBrilliance"] = {
label = "Catha's Brilliance",
incrementalEffectiveness = 0.054999999701977,
statDescriptionScope = "gem_stat_descriptions",
statMap = {
-- Catha's Brilliance ignites enemies for a % of the minion's max life, the
-- same mechanic as Infernal Legion: wire the stat to the IL base-damage
-- multiplier and grant the companion the IL skill so the ignite is modelled.
["support_minions_ignite_for_%_max_life"] = {
mod("MinionModifier", "LIST", { mod = mod("Multiplier:InfernalLegionBaseDamage", "BASE", nil) }),
mod("ExtraMinionSkill", "LIST", { skillId = "InfernalLegion" }),
},
},
baseFlags = {
},
constantStats = {
Expand Down
15 changes: 6 additions & 9 deletions src/Data/Skills/sup_str.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4634,9 +4634,8 @@ skills["SupportInfernalLegionPlayer"] = {
mod("MinionModifier", "LIST", { mod = mod("FireDegen", "BASE", nil, 0, 0, { type = "PerStat", stat = "Life" }, { type = "GlobalEffect", effectType = "Buff" }) }),
div = 6000,
},
["support_minion_instability_minion_base_fire_area_damage_per_minute"] = {
mod("MinionModifier", "LIST", { mod = mod("Multiplier:InfernalLegionBaseDamage", "BASE", nil, 0, 0, { type = "PercentStat", stat = "Life", percent = 1 }) }),
div = 60,
["support_minions_ignite_for_%_max_life"] = {
mod("MinionModifier", "LIST", { mod = mod("Multiplier:InfernalLegionBaseDamage", "BASE", nil) }),
mod("ExtraMinionSkill", "LIST", { skillId = "InfernalLegion" }),
},
},
Expand Down Expand Up @@ -4677,9 +4676,8 @@ skills["SupportInfernalLegionPlayerTwo"] = {
mod("MinionModifier", "LIST", { mod = mod("FireDegen", "BASE", nil, 0, 0, { type = "PerStat", stat = "Life" }, { type = "GlobalEffect", effectType = "Buff" }) }),
div = 6000,
},
["support_minion_instability_minion_base_fire_area_damage_per_minute"] = {
mod("MinionModifier", "LIST", { mod = mod("Multiplier:InfernalLegionBaseDamage", "BASE", nil, 0, 0, { type = "PercentStat", stat = "Life", percent = 1 }) }),
div = 60,
["support_minions_ignite_for_%_max_life"] = {
mod("MinionModifier", "LIST", { mod = mod("Multiplier:InfernalLegionBaseDamage", "BASE", nil) }),
mod("ExtraMinionSkill", "LIST", { skillId = "InfernalLegion" }),
},
},
Expand Down Expand Up @@ -4722,9 +4720,8 @@ skills["SupportInfernalLegionPlayerThree"] = {
mod("MinionModifier", "LIST", { mod = mod("FireDegen", "BASE", nil, 0, 0, { type = "PerStat", stat = "Life" }, { type = "GlobalEffect", effectType = "Buff" }) }),
div = 6000,
},
["support_minion_instability_minion_base_fire_area_damage_per_minute"] = {
mod("MinionModifier", "LIST", { mod = mod("Multiplier:InfernalLegionBaseDamage", "BASE", nil, 0, 0, { type = "PercentStat", stat = "Life", percent = 1 }) }),
div = 60,
["support_minions_ignite_for_%_max_life"] = {
mod("MinionModifier", "LIST", { mod = mod("Multiplier:InfernalLegionBaseDamage", "BASE", nil) }),
mod("ExtraMinionSkill", "LIST", { skillId = "InfernalLegion" }),
},
},
Expand Down
52 changes: 52 additions & 0 deletions src/Export/Skills/minion.txt
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,58 @@ skills["MinionInstability"] = {
}
}

skills["InfernalLegion"] = {
name = "Infernal Legion",
hidden = true,
skillTypes = {
[SkillType.Damage] = true,
[SkillType.Area] = true,
[SkillType.Fire] = true,
[SkillType.CausesBurning] = true,
},
qualityStats = {
},
levels = {
[1] = { critChance = 0, levelRequirement = 0, }, -- only crits from enemy Critical Weakness
},
preDamageFunc = function(activeSkill, output)
local skillData = activeSkill.skillData
local sml = activeSkill.skillModList
local cfg = activeSkill.skillCfg
local base = output.Life * skillData.selfFireExplosionLifeMultiplier
skillData.FireBonusMin = base
skillData.FireBonusMax = base
-- minion's increased crit chance/damage are half-effective for IL
sml:NewMod("CritChance", "INC", -0.5 * sml:Sum("INC", cfg, "CritChance"), "Infernal Legion")
sml:NewMod("CritMultiplier", "INC", -0.5 * sml:Sum("INC", cfg, "CritMultiplier"), "Infernal Legion")
end,
statSets = {
[1] = {
label = "Infernal Legion",
incrementalEffectiveness = 0,
statDescriptionScope = "skill_stat_descriptions",
baseFlags = {
area = true,
fire = true,
},
baseMods = {
skill("selfFireExplosionLifeMultiplier", 0.01, { type = "Multiplier", var = "InfernalLegionBaseDamage" }),
skill("pseudoHitAilment", "Ignite"), -- fake hit: only seeds Ignite, no hit damage
skill("timeOverride", 1.25), -- fixed ~0.8/sec ignite cadence, not attack speed
mod("EnemyIgniteChance", "BASE", 100),
mod("CritMultiplier", "BASE", -50), -- 50% base crit bonus, not the minion's 100%
},
constantStats = {
},
stats = {
},
levels = {
[1] = { },
},
},
}
}

#addSkillTypes Melee MeleeSingleTarget Area
#skill GAAnimateWeaponMaceSlam Mace Slam
#set GAAnimateWeaponMaceSlam
Expand Down
9 changes: 9 additions & 0 deletions src/Export/Skills/other.txt
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,15 @@ statMap = {

#skill SupportCathasBrilliance
#set SupportCathasBrilliance
statMap = {
-- Catha's Brilliance ignites enemies for a % of the minion's max life, the
-- same mechanic as Infernal Legion: wire the stat to the IL base-damage
-- multiplier and grant the companion the IL skill so the ignite is modelled.
["support_minions_ignite_for_%_max_life"] = {
mod("MinionModifier", "LIST", { mod = mod("Multiplier:InfernalLegionBaseDamage", "BASE", nil) }),
mod("ExtraMinionSkill", "LIST", { skillId = "InfernalLegion" }),
},
},
#mods
#skillEnd

Expand Down
Loading