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
220 changes: 220 additions & 0 deletions spec/System/TestOffence_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
describe("TestOffence", function()
before_each(function()
newBuild()
end)

teardown(function()
-- newBuild() takes care of resetting everything in setup()
end)

-- Asserts actual is within a relative tolerance of expected, e.g. 0.005 = 0.5%
local function assertNearRelative(expected, actual, tolerance, msg)
assert.is_true(math.abs(expected - actual) / expected <= tolerance,
string.format("%s: expected ~%.2f (within %.1f%%), got %.2f", msg, expected, tolerance * 100, actual))
end

it("parses more/less/increased/reduced minimum and maximum damage of every type", function()
build.itemsTab:CreateDisplayItemFromRaw([[
New Item
Coral Amulet
25% more Maximum Lightning Damage
50% less Minimum Lightning Damage
10% increased Maximum Cold Damage
12% reduced Minimum Physical Damage
20% more Maximum Chaos Damage
30% more Maximum Fire Damage
]])
build.itemsTab:AddDisplayItem()
runCallback("OnFrame")

local modDB = build.calcsTab.mainEnv.player.modDB

-- "more"/"less" -> MORE modifier (More() returns the aggregate multiplier)
assert.are.equals(1.25, modDB:More(nil, "MaxLightningDamage"))
assert.are.equals(0.5, modDB:More(nil, "MinLightningDamage"))
assert.are.equals(1.2, modDB:More(nil, "MaxChaosDamage"))
assert.are.equals(1.3, modDB:More(nil, "MaxFireDamage"))

-- "increased"/"reduced" -> INC modifier
assert.are.equals(10, modDB:Sum("INC", nil, "MaxColdDamage"))
assert.are.equals(-12, modDB:Sum("INC", nil, "MinPhysicalDamage"))

-- sanity: these must not have leaked into the wrong stat/type
assert.are.equals(1, modDB:More(nil, "MinChaosDamage"))
assert.are.equals(0, modDB:Sum("INC", nil, "MaxLightningDamage"))
end)

-- calcDamage rounds each min/max to an integer before our multipliers' effects can be
-- observed, so scaling the rounded baseline can differ from the real value by ~1
local function assertNear(expected, actual, msg)
assert.is_true(math.abs(expected - actual) <= 2, string.format("%s: expected ~%.2f, got %.2f", msg, expected, actual))
end

it("applies min/max damage mods to an actual skill in CalcOffence", function()
build.skillsTab:PasteSocketGroup("Slot: Body Armour\nArc 20/0 1\n")
runCallback("OnFrame")

local baseMin = build.calcsTab.calcsOutput.LightningMin
local baseMax = build.calcsTab.calcsOutput.LightningMax
assert.is_true(baseMax > 0)
assert.is_true(baseMin > 0)

-- MORE path: "more"/"less" scale only the targeted end of the roll
build.itemsTab:CreateDisplayItemFromRaw([[
New Item
Coral Amulet
25% more Maximum Lightning Damage
50% less Minimum Lightning Damage
]])
build.itemsTab:AddDisplayItem()
runCallback("OnFrame")

assertNear(baseMax * 1.25, build.calcsTab.calcsOutput.LightningMax, "25%% more max")
assertNear(baseMin * 0.5, build.calcsTab.calcsOutput.LightningMin, "50%% less min")

-- INC path: "increased" stacks multiplicatively with the MORE factors above
build.itemsTab:CreateDisplayItemFromRaw([[
New Item
Coral Ring
40% increased Maximum Lightning Damage
100% increased Minimum Lightning Damage
]])
build.itemsTab:AddDisplayItem()
runCallback("OnFrame")

assertNear(baseMax * 1.25 * 1.4, build.calcsTab.calcsOutput.LightningMax, "more + increased max")
assertNear(baseMin * 0.5 * 2, build.calcsTab.calcsOutput.LightningMin, "less + increased min")
end)

it("parses universal cannot deal/deal no non-<type> damage for player and minions", function()
build.skillsTab:PasteSocketGroup("Slot: Body Armour\nArc 20/0 1\n")
runCallback("OnFrame")
assert.is_true(build.calcsTab.calcsOutput.LightningMax > 0)

build.itemsTab:CreateDisplayItemFromRaw([[
New Item
Coral Amulet
Cannot deal non-Fire Damage
Minions deal no non-Fire Damage
]])
build.itemsTab:AddDisplayItem()
runCallback("OnFrame")

local modDB = build.calcsTab.mainEnv.player.modDB
assert.truthy(modDB:Flag(nil, "DealNoPhysical"))
assert.truthy(modDB:Flag(nil, "DealNoLightning"))
assert.truthy(modDB:Flag(nil, "DealNoCold"))
assert.truthy(modDB:Flag(nil, "DealNoChaos"))
assert.is_true(not (modDB:Flag(nil, "DealNoFire")))

-- minion wording wraps the same flags in MinionModifier
local minionDealNo = { }
for _, value in ipairs(modDB:List(nil, "MinionModifier")) do
if value.mod and value.mod.name:match("^DealNo") then
minionDealNo[value.mod.name] = true
end
end
assert.truthy(minionDealNo["DealNoPhysical"])
assert.truthy(minionDealNo["DealNoLightning"])
assert.truthy(minionDealNo["DealNoCold"])
assert.truthy(minionDealNo["DealNoChaos"])
assert.is_true(not (minionDealNo["DealNoFire"]))

-- Arc is pure lightning, so the player can no longer deal damage with it
assert.are.equals(0, build.calcsTab.calcsOutput.LightningMax)
end)

it("keeps deal no non-elemental damage as its own literal", function()
build.itemsTab:CreateDisplayItemFromRaw([[
New Item
Coral Amulet
Deal no non-Elemental Damage
]])
build.itemsTab:AddDisplayItem()
runCallback("OnFrame")

local modDB = build.calcsTab.mainEnv.player.modDB
assert.truthy(modDB:Flag(nil, "DealNoPhysical"))
assert.truthy(modDB:Flag(nil, "DealNoChaos"))
assert.is_true(not (modDB:Flag(nil, "DealNoLightning")))
assert.is_true(not (modDB:Flag(nil, "DealNoCold")))
assert.is_true(not (modDB:Flag(nil, "DealNoFire")))
end)

it("enemies in your chilling areas take damage increased by the area's chill effect", function()
build.skillsTab:PasteSocketGroup("Slot: Body Armour\nVortex 20/0 1\n")
runCallback("OnFrame")

local baseAvg = build.calcsTab.mainOutput.AverageDamage
assert.is_true(baseAvg > 0)
-- the chill effect currently applied to the enemy (here, from Vortex's chilling area)
local currentChill = build.calcsTab.mainOutput.CurrentChill
assert.is_true(currentChill ~= nil and currentChill > 0)

build.itemsTab:CreateDisplayItemFromRaw([[
New Item
Coral Amulet
Enemies in your Chilling Areas have Cold Damage taken increased by Chill Effect
]])
build.itemsTab:AddDisplayItem()
runCallback("OnFrame")

-- config checkbox not ticked -> enemy is not in the area -> no change
assert.are.equals(baseAvg, build.calcsTab.mainOutput.AverageDamage)

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

-- Vortex deals pure cold damage, so the ColdDamageTaken increase scales all of it
local scaledAvg = baseAvg * (1 + currentChill / 100)
assertNearRelative(scaledAvg, build.calcsTab.mainOutput.AverageDamage, 0.005,
string.format("base %.2f scaled by %d%% current chill", baseAvg, currentChill))

-- the paired "Chilled by your Hits" wording must not stack with the chilling area one
build.itemsTab:CreateDisplayItemFromRaw([[
New Item
Coral Ring
Enemies Chilled by your Hits have Cold Damage taken increased by Chill Effect
]])
build.itemsTab:AddDisplayItem()
runCallback("OnFrame")

assertNearRelative(scaledAvg, build.calcsTab.mainOutput.AverageDamage, 0.005,
string.format("both wordings must apply only once (base %.2f, %d%% chill)", baseAvg, currentChill))
end)

-- "Base <ailment> Duration is X seconds" overrides the fixed base duration of the damaging ailments
for _, case in ipairs({
{ ailment = "Ignite", skill = "Fireball", chanceMod = "25% chance to Ignite", gameBase = 4 },
{ ailment = "Bleeding", skill = "Double Strike", chanceMod = "25% chance to cause Bleeding on Hit", gameBase = 5, output = "BleedDuration" },
{ ailment = "Poison", skill = "Double Strike", chanceMod = "25% chance to Poison on Hit", gameBase = 2 },
}) do
it("supports base " .. case.ailment .. " duration override", function()
local outputName = case.output or (case.ailment .. "Duration")
build.itemsTab:CreateDisplayItemFromRaw([[
New Item
Rusted Sword
]])
build.itemsTab:AddDisplayItem()
build.skillsTab:PasteSocketGroup("Slot: Body Armour\n" .. case.skill .. " 20/0 1\n")
build.itemsTab:CreateDisplayItemFromRaw([[
New Item
Coral Amulet
]] .. case.chanceMod)
build.itemsTab:AddDisplayItem()
runCallback("OnFrame")

assert.are.equals(case.gameBase, build.calcsTab.mainOutput[outputName])

build.itemsTab:CreateDisplayItemFromRaw([[
New Item
Coral Ring
Base ]] .. case.ailment .. [[ Duration is 1 second]])
build.itemsTab:AddDisplayItem()
runCallback("OnFrame")

assert.are.equals(1, build.calcsTab.mainOutput[outputName])
end)
end
end)
6 changes: 3 additions & 3 deletions src/Data/ModCache.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8230,9 +8230,9 @@ c["Deal no Cold Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoCold",type="F
c["Deal no Damage when not on Low Life"]={{[1]={[1]={neg=true,type="Condition",var="LowLife"},flags=0,keywordFlags=0,name="DealNoDamage",type="FLAG",value=true}},nil}
c["Deal no Elemental Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true}},nil}
c["Deal no Non-Elemental Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}},nil}
c["Deal no Non-Fire Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}},nil}
c["Deal no Non-Fire Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}},nil}
c["Deal no Non-Lightning Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}},nil}
c["Deal no Non-Physical Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}},nil}
c["Deal no Non-Physical Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}},nil}
c["Deal no Physical Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true}},nil}
c["Deal no Physical or Elemental Damage"]={{[1]={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="DealNoCold",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true},[4]={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true}},nil}
c["Deal up to 15% more Melee Damage to Enemies, based on proximity"]={{[1]={[1]={ramp={[1]=1,[2]=0},type="MeleeProximity"},flags=257,keywordFlags=0,name="Damage",type="MORE",value=15}},nil}
Expand Down Expand Up @@ -9905,7 +9905,7 @@ c["Minions deal 80% increased Damage"]={{[1]={flags=0,keywordFlags=0,name="Minio
c["Minions deal 80% increased Damage if you have Warcried Recently"]={{[1]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={[1]={actor="parent",type="ActorCondition",var="UsedWarcryRecently"},flags=0,keywordFlags=0,name="Damage",type="INC",value=80}}}},nil}
c["Minions deal 9 to 15 additional Cold Damage"]={{[1]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="ColdMin",type="BASE",value=9}}},[2]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="ColdMax",type="BASE",value=15}}}},nil}
c["Minions deal 96 to 144 additional Physical Damage"]={{[1]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="PhysicalMin",type="BASE",value=96}}},[2]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="PhysicalMax",type="BASE",value=144}}}},nil}
c["Minions deal no Non-Cold Damage"]={{[1]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true}}},[2]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true}}},[3]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true}}},[4]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}}}},nil}
c["Minions deal no Non-Cold Damage"]={{[1]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="DealNoPhysical",type="FLAG",value=true}}},[2]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="DealNoFire",type="FLAG",value=true}}},[3]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="DealNoLightning",type="FLAG",value=true}}},[4]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="DealNoChaos",type="FLAG",value=true}}}},nil}
c["Minions from Herald Skills deal 25% more Damage"]={{[1]={[1]={skillType=62,type="SkillType"},flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="Damage",type="MORE",value=25}}}},nil}
c["Minions gain 18% of Elemental Damage as Extra Chaos Damage"]={{[1]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="ElementalDamageGainAsChaos",type="BASE",value=18}}}},nil}
c["Minions gain 20% of Elemental Damage as Extra Chaos Damage"]={{[1]={flags=0,keywordFlags=0,name="MinionModifier",type="LIST",value={mod={flags=0,keywordFlags=0,name="ElementalDamageGainAsChaos",type="BASE",value=20}}}},nil}
Expand Down
17 changes: 7 additions & 10 deletions src/Modules/CalcOffence.lua
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ local function calcDamage(activeSkill, output, cfg, breakdown, damageType, typeF
local genericMoreMaxDamage = skillModList:More(cfg, "MaxDamage")
local moreMinDamage = skillModList:More(cfg, "Min"..damageType.."Damage")
local moreMaxDamage = skillModList:More(cfg, "Max"..damageType.."Damage")
local incMinDamage = 1 + skillModList:Sum("INC", cfg, "Min"..damageType.."Damage") / 100
local incMaxDamage = 1 + skillModList:Sum("INC", cfg, "Max"..damageType.."Damage") / 100

if breakdown then
t_insert(breakdown.damageTypes, {
Expand All @@ -129,8 +131,8 @@ local function calcDamage(activeSkill, output, cfg, breakdown, damageType, typeF
})
end

return round(((baseMin * inc * more) * genericMoreMinDamage + addMin) * moreMinDamage),
round(((baseMax * inc * more) * genericMoreMaxDamage + addMax) * moreMaxDamage)
return round(((baseMin * inc * more) * genericMoreMinDamage + addMin) * moreMinDamage * incMinDamage),
round(((baseMax * inc * more) * genericMoreMaxDamage + addMax) * moreMaxDamage * incMaxDamage)
end

local function calcAilmentSourceDamage(activeSkill, output, cfg, breakdown, damageType, typeFlags)
Expand Down Expand Up @@ -4201,7 +4203,7 @@ function calcs.offence(env, actor, activeSkill)
local maxStacks = skillModList:Override(cfg, "BleedStacksMax") or skillModList:Sum("BASE", cfg, "BleedStacksMax")
local overrideStackPotential = skillModList:Override(nil, "BleedStackPotentialOverride") and skillModList:Override(nil, "BleedStackPotentialOverride") / maxStacks
globalOutput.BleedStacksMax = maxStacks
local durationBase = skillData.bleedDurationIsSkillDuration and skillData.duration or data.misc.BleedDurationBase
local durationBase = skillModList:Override(dotCfg, "BleedDurationBase") or (skillData.bleedDurationIsSkillDuration and skillData.duration) or data.misc.BleedDurationBase
local durationMod = calcLib.mod(skillModList, dotCfg, "EnemyBleedDuration", "EnemyAilmentDuration", "DamagingAilmentDuration", skillData.bleedIsSkillEffect and "Duration" or nil) * calcLib.mod(enemyDB, nil, "SelfBleedDuration", "SelfAilmentDuration") / calcLib.mod(enemyDB, dotCfg, "BleedExpireRate")
durationMod = m_max(durationMod, 0)
local rateMod = calcLib.mod(skillModList, cfg, "BleedFaster") + enemyDB:Sum("INC", nil, "SelfBleedFaster") / 100
Expand Down Expand Up @@ -4473,12 +4475,7 @@ function calcs.offence(env, actor, activeSkill)
breakdown.PoisonChaos = { damageTypes = { } }
end
local rateMod = calcLib.mod(skillModList, cfg, "PoisonFaster") + enemyDB:Sum("INC", nil, "SelfPoisonFaster") / 100
local durationBase
if skillData.poisonDurationIsSkillDuration then
durationBase = skillData.duration
else
durationBase = data.misc.PoisonDurationBase
end
local durationBase = skillModList:Override(dotCfg, "PoisonDurationBase") or (skillData.poisonDurationIsSkillDuration and skillData.duration) or data.misc.PoisonDurationBase
local durationMod = calcLib.mod(skillModList, dotCfg, "EnemyPoisonDuration", "EnemyAilmentDuration", "DamagingAilmentDuration", skillData.poisonIsSkillEffect and "Duration" or nil) * calcLib.mod(enemyDB, nil, "SelfPoisonDuration", "SelfAilmentDuration")
durationMod = m_max(durationMod, 0)
globalOutput.PoisonDuration = durationBase * durationMod / rateMod * debuffDurationMult
Expand Down Expand Up @@ -4794,7 +4791,7 @@ function calcs.offence(env, actor, activeSkill)
globalOutput.IgniteStacksMax = maxStacks

local rateMod = (calcLib.mod(skillModList, cfg, "IgniteBurnFaster") + enemyDB:Sum("INC", nil, "SelfIgniteBurnFaster") / 100) / calcLib.mod(skillModList, cfg, "IgniteBurnSlower")
local durationBase = data.misc.IgniteDurationBase
local durationBase = skillModList:Override(dotCfg, "IgniteDurationBase") or data.misc.IgniteDurationBase
local durationMod = m_max(calcLib.mod(skillModList, dotCfg, "EnemyIgniteDuration", "EnemyAilmentDuration", "EnemyElementalAilmentDuration", "DamagingAilmentDuration") * calcLib.mod(enemyDB, nil, "SelfIgniteDuration", "SelfAilmentDuration", "SelfElementalAilmentDuration"), 0)
durationMod = m_max(durationMod, 0)
globalOutput.IgniteDuration = durationBase * durationMod / rateMod * debuffDurationMult
Expand Down
6 changes: 6 additions & 0 deletions src/Modules/CalcPerform.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3349,6 +3349,12 @@ function calcs.perform(env, skipEHP)
local mods = { modLib.createMod("ActionSpeed", "INC", -num, "Chill", { type = "Condition", var = "Chilled" }) }
if modDB:Flag(nil, "ChillEffectIncDamageTaken") then
t_insert(mods, modLib.createMod("DamageTaken", "INC", num, "Ahuana's Bite", { type = "Condition", var = "Chilled" }))
elseif modDB:Flag(nil, "ChillEffectIncColdDamageTaken") then
t_insert(mods, modLib.createMod("ColdDamageTaken", "INC", num, "Chilled by Hits", { type = "Condition", var = "Chilled" }))
-- Scales off the current chill effect on the enemy; the flag itself is gated on the
-- enemy being in a chilling area (config option)
elseif modDB:Flag(nil, "ChillingAreaIncColdDamageTaken") then
t_insert(mods, modLib.createMod("ColdDamageTaken", "INC", num, "Chilling Area", { type = "Condition", var = "Chilled" }))
elseif output.HasBonechill and (hasGuaranteedBonechill or enemyDB:Sum("BASE", nil, "ChillVal") > 0) then
t_insert(mods, modLib.createMod("ColdDamageTaken", "INC", num, "Bonechill", { type = "Condition", var = "Chilled" }))
end
Expand Down
Loading
Loading