|
| 1 | +describe("TestOffence", function() |
| 2 | + before_each(function() |
| 3 | + newBuild() |
| 4 | + end) |
| 5 | + |
| 6 | + teardown(function() |
| 7 | + -- newBuild() takes care of resetting everything in setup() |
| 8 | + end) |
| 9 | + |
| 10 | + -- Asserts actual is within a relative tolerance of expected, e.g. 0.005 = 0.5% |
| 11 | + local function assertNearRelative(expected, actual, tolerance, msg) |
| 12 | + assert.is_true(math.abs(expected - actual) / expected <= tolerance, |
| 13 | + string.format("%s: expected ~%.2f (within %.1f%%), got %.2f", msg, expected, tolerance * 100, actual)) |
| 14 | + end |
| 15 | + |
| 16 | + it("parses more/less/increased/reduced minimum and maximum damage of every type", function() |
| 17 | + build.itemsTab:CreateDisplayItemFromRaw([[ |
| 18 | + New Item |
| 19 | + Coral Amulet |
| 20 | + 25% more Maximum Lightning Damage |
| 21 | + 50% less Minimum Lightning Damage |
| 22 | + 10% increased Maximum Cold Damage |
| 23 | + 12% reduced Minimum Physical Damage |
| 24 | + 20% more Maximum Chaos Damage |
| 25 | + 30% more Maximum Fire Damage |
| 26 | + ]]) |
| 27 | + build.itemsTab:AddDisplayItem() |
| 28 | + runCallback("OnFrame") |
| 29 | + |
| 30 | + local modDB = build.calcsTab.mainEnv.player.modDB |
| 31 | + |
| 32 | + -- "more"/"less" -> MORE modifier (More() returns the aggregate multiplier) |
| 33 | + assert.are.equals(1.25, modDB:More(nil, "MaxLightningDamage")) |
| 34 | + assert.are.equals(0.5, modDB:More(nil, "MinLightningDamage")) |
| 35 | + assert.are.equals(1.2, modDB:More(nil, "MaxChaosDamage")) |
| 36 | + assert.are.equals(1.3, modDB:More(nil, "MaxFireDamage")) |
| 37 | + |
| 38 | + -- "increased"/"reduced" -> INC modifier |
| 39 | + assert.are.equals(10, modDB:Sum("INC", nil, "MaxColdDamage")) |
| 40 | + assert.are.equals(-12, modDB:Sum("INC", nil, "MinPhysicalDamage")) |
| 41 | + |
| 42 | + -- sanity: these must not have leaked into the wrong stat/type |
| 43 | + assert.are.equals(1, modDB:More(nil, "MinChaosDamage")) |
| 44 | + assert.are.equals(0, modDB:Sum("INC", nil, "MaxLightningDamage")) |
| 45 | + end) |
| 46 | + |
| 47 | + -- calcDamage rounds each min/max to an integer before our multipliers' effects can be |
| 48 | + -- observed, so scaling the rounded baseline can differ from the real value by ~1 |
| 49 | + local function assertNear(expected, actual, msg) |
| 50 | + assert.is_true(math.abs(expected - actual) <= 2, string.format("%s: expected ~%.2f, got %.2f", msg, expected, actual)) |
| 51 | + end |
| 52 | + |
| 53 | + it("applies min/max damage mods to an actual skill in CalcOffence", function() |
| 54 | + build.skillsTab:PasteSocketGroup("Slot: Body Armour\nArc 20/0 1\n") |
| 55 | + runCallback("OnFrame") |
| 56 | + |
| 57 | + local baseMin = build.calcsTab.calcsOutput.LightningMin |
| 58 | + local baseMax = build.calcsTab.calcsOutput.LightningMax |
| 59 | + assert.is_true(baseMax > 0) |
| 60 | + assert.is_true(baseMin > 0) |
| 61 | + |
| 62 | + -- MORE path: "more"/"less" scale only the targeted end of the roll |
| 63 | + build.itemsTab:CreateDisplayItemFromRaw([[ |
| 64 | + New Item |
| 65 | + Coral Amulet |
| 66 | + 25% more Maximum Lightning Damage |
| 67 | + 50% less Minimum Lightning Damage |
| 68 | + ]]) |
| 69 | + build.itemsTab:AddDisplayItem() |
| 70 | + runCallback("OnFrame") |
| 71 | + |
| 72 | + assertNear(baseMax * 1.25, build.calcsTab.calcsOutput.LightningMax, "25%% more max") |
| 73 | + assertNear(baseMin * 0.5, build.calcsTab.calcsOutput.LightningMin, "50%% less min") |
| 74 | + |
| 75 | + -- INC path: "increased" stacks multiplicatively with the MORE factors above |
| 76 | + build.itemsTab:CreateDisplayItemFromRaw([[ |
| 77 | + New Item |
| 78 | + Coral Ring |
| 79 | + 40% increased Maximum Lightning Damage |
| 80 | + 100% increased Minimum Lightning Damage |
| 81 | + ]]) |
| 82 | + build.itemsTab:AddDisplayItem() |
| 83 | + runCallback("OnFrame") |
| 84 | + |
| 85 | + assertNear(baseMax * 1.25 * 1.4, build.calcsTab.calcsOutput.LightningMax, "more + increased max") |
| 86 | + assertNear(baseMin * 0.5 * 2, build.calcsTab.calcsOutput.LightningMin, "less + increased min") |
| 87 | + end) |
| 88 | + |
| 89 | + it("applies minimum and maximum attack damage mods", function() |
| 90 | + build.itemsTab:CreateDisplayItemFromRaw([[ |
| 91 | + New Item |
| 92 | + Rusted Sword |
| 93 | + ]]) |
| 94 | + build.itemsTab:AddDisplayItem() |
| 95 | + build.skillsTab:PasteSocketGroup("Double Strike 20/0 1") |
| 96 | + runCallback("OnFrame") |
| 97 | + |
| 98 | + local baseMin = build.calcsTab.mainOutput.MainHand.TotalMin |
| 99 | + local baseMax = build.calcsTab.mainOutput.MainHand.TotalMax |
| 100 | + |
| 101 | + build.itemsTab:CreateDisplayItemFromRaw([[ |
| 102 | + New Item |
| 103 | + Coral Amulet |
| 104 | + 50% less Minimum Attack Damage |
| 105 | + 100% more Maximum Attack Damage |
| 106 | + ]]) |
| 107 | + build.itemsTab:AddDisplayItem() |
| 108 | + runCallback("OnFrame") |
| 109 | + |
| 110 | + assertNear(baseMin * 0.5, build.calcsTab.mainOutput.MainHand.TotalMin, "50%% less attack min") |
| 111 | + assertNear(baseMax * 2, build.calcsTab.mainOutput.MainHand.TotalMax, "100%% more attack max") |
| 112 | + end) |
| 113 | + |
| 114 | + it("parses universal cannot deal/deal no non-<type> damage for player and minions", function() |
| 115 | + build.skillsTab:PasteSocketGroup("Slot: Body Armour\nArc 20/0 1\n") |
| 116 | + runCallback("OnFrame") |
| 117 | + assert.is_true(build.calcsTab.calcsOutput.LightningMax > 0) |
| 118 | + |
| 119 | + build.itemsTab:CreateDisplayItemFromRaw([[ |
| 120 | + New Item |
| 121 | + Coral Amulet |
| 122 | + Cannot deal non-Fire Damage |
| 123 | + Minions deal no non-Fire Damage |
| 124 | + ]]) |
| 125 | + build.itemsTab:AddDisplayItem() |
| 126 | + runCallback("OnFrame") |
| 127 | + |
| 128 | + local modDB = build.calcsTab.mainEnv.player.modDB |
| 129 | + assert.truthy(modDB:Flag(nil, "DealNoPhysical")) |
| 130 | + assert.truthy(modDB:Flag(nil, "DealNoLightning")) |
| 131 | + assert.truthy(modDB:Flag(nil, "DealNoCold")) |
| 132 | + assert.truthy(modDB:Flag(nil, "DealNoChaos")) |
| 133 | + assert.is_true(not (modDB:Flag(nil, "DealNoFire"))) |
| 134 | + |
| 135 | + -- minion wording wraps the same flags in MinionModifier |
| 136 | + local minionDealNo = { } |
| 137 | + for _, value in ipairs(modDB:List(nil, "MinionModifier")) do |
| 138 | + if value.mod and value.mod.name:match("^DealNo") then |
| 139 | + minionDealNo[value.mod.name] = true |
| 140 | + end |
| 141 | + end |
| 142 | + assert.truthy(minionDealNo["DealNoPhysical"]) |
| 143 | + assert.truthy(minionDealNo["DealNoLightning"]) |
| 144 | + assert.truthy(minionDealNo["DealNoCold"]) |
| 145 | + assert.truthy(minionDealNo["DealNoChaos"]) |
| 146 | + assert.is_true(not (minionDealNo["DealNoFire"])) |
| 147 | + |
| 148 | + -- Arc is pure lightning, so the player can no longer deal damage with it |
| 149 | + assert.are.equals(0, build.calcsTab.calcsOutput.LightningMax) |
| 150 | + end) |
| 151 | + |
| 152 | + it("keeps deal no non-elemental damage as its own literal", function() |
| 153 | + build.itemsTab:CreateDisplayItemFromRaw([[ |
| 154 | + New Item |
| 155 | + Coral Amulet |
| 156 | + Deal no non-Elemental Damage |
| 157 | + ]]) |
| 158 | + build.itemsTab:AddDisplayItem() |
| 159 | + runCallback("OnFrame") |
| 160 | + |
| 161 | + local modDB = build.calcsTab.mainEnv.player.modDB |
| 162 | + assert.truthy(modDB:Flag(nil, "DealNoPhysical")) |
| 163 | + assert.truthy(modDB:Flag(nil, "DealNoChaos")) |
| 164 | + assert.is_true(not (modDB:Flag(nil, "DealNoLightning"))) |
| 165 | + assert.is_true(not (modDB:Flag(nil, "DealNoCold"))) |
| 166 | + assert.is_true(not (modDB:Flag(nil, "DealNoFire"))) |
| 167 | + end) |
| 168 | + |
| 169 | + it("enemies in your chilling areas take damage increased by the area's chill effect", function() |
| 170 | + build.skillsTab:PasteSocketGroup("Slot: Body Armour\nVortex 20/0 1\n") |
| 171 | + runCallback("OnFrame") |
| 172 | + |
| 173 | + local baseAvg = build.calcsTab.mainOutput.AverageDamage |
| 174 | + assert.is_true(baseAvg > 0) |
| 175 | + -- the chill effect currently applied to the enemy (here, from Vortex's chilling area) |
| 176 | + local currentChill = build.calcsTab.mainOutput.CurrentChill |
| 177 | + assert.is_true(currentChill ~= nil and currentChill > 0) |
| 178 | + |
| 179 | + build.itemsTab:CreateDisplayItemFromRaw([[ |
| 180 | + New Item |
| 181 | + Coral Amulet |
| 182 | + Enemies in your Chilling Areas have Cold Damage taken increased by Chill Effect |
| 183 | + ]]) |
| 184 | + build.itemsTab:AddDisplayItem() |
| 185 | + runCallback("OnFrame") |
| 186 | + |
| 187 | + -- config checkbox not ticked -> enemy is not in the area -> no change |
| 188 | + assert.are.equals(baseAvg, build.calcsTab.mainOutput.AverageDamage) |
| 189 | + |
| 190 | + build.configTab.input.conditionEnemyInChillingArea = true |
| 191 | + build.configTab:BuildModList() |
| 192 | + runCallback("OnFrame") |
| 193 | + |
| 194 | + -- Vortex deals pure cold damage, so the ColdDamageTaken increase scales all of it |
| 195 | + local scaledAvg = baseAvg * (1 + currentChill / 100) |
| 196 | + assertNearRelative(scaledAvg, build.calcsTab.mainOutput.AverageDamage, 0.005, |
| 197 | + string.format("base %.2f scaled by %d%% current chill", baseAvg, currentChill)) |
| 198 | + |
| 199 | + -- the paired "Chilled by your Hits" wording must not stack with the chilling area one |
| 200 | + build.itemsTab:CreateDisplayItemFromRaw([[ |
| 201 | + New Item |
| 202 | + Coral Ring |
| 203 | + Enemies Chilled by your Hits have Cold Damage taken increased by Chill Effect |
| 204 | + ]]) |
| 205 | + build.itemsTab:AddDisplayItem() |
| 206 | + runCallback("OnFrame") |
| 207 | + |
| 208 | + assertNearRelative(scaledAvg, build.calcsTab.mainOutput.AverageDamage, 0.005, |
| 209 | + string.format("both wordings must apply only once (base %.2f, %d%% chill)", baseAvg, currentChill)) |
| 210 | + end) |
| 211 | + |
| 212 | + -- "Base <ailment> Duration is X seconds" overrides the fixed base duration of the damaging ailments |
| 213 | + for _, case in ipairs({ |
| 214 | + { ailment = "Ignite", skill = "Fireball", chanceMod = "25% chance to Ignite", gameBase = 4 }, |
| 215 | + { ailment = "Bleeding", skill = "Double Strike", chanceMod = "25% chance to cause Bleeding on Hit", gameBase = 5, output = "BleedDuration" }, |
| 216 | + { ailment = "Poison", skill = "Double Strike", chanceMod = "25% chance to Poison on Hit", gameBase = 2 }, |
| 217 | + }) do |
| 218 | + it("supports base " .. case.ailment .. " duration override", function() |
| 219 | + local outputName = case.output or (case.ailment .. "Duration") |
| 220 | + build.itemsTab:CreateDisplayItemFromRaw([[ |
| 221 | + New Item |
| 222 | + Rusted Sword |
| 223 | + ]]) |
| 224 | + build.itemsTab:AddDisplayItem() |
| 225 | + build.skillsTab:PasteSocketGroup("Slot: Body Armour\n" .. case.skill .. " 20/0 1\n") |
| 226 | + build.itemsTab:CreateDisplayItemFromRaw([[ |
| 227 | + New Item |
| 228 | + Coral Amulet |
| 229 | + ]] .. case.chanceMod) |
| 230 | + build.itemsTab:AddDisplayItem() |
| 231 | + runCallback("OnFrame") |
| 232 | + |
| 233 | + assert.are.equals(case.gameBase, build.calcsTab.mainOutput[outputName]) |
| 234 | + |
| 235 | + build.itemsTab:CreateDisplayItemFromRaw([[ |
| 236 | + New Item |
| 237 | + Coral Ring |
| 238 | + Base ]] .. case.ailment .. [[ Duration is 1 second]]) |
| 239 | + build.itemsTab:AddDisplayItem() |
| 240 | + runCallback("OnFrame") |
| 241 | + |
| 242 | + assert.are.equals(1, build.calcsTab.mainOutput[outputName]) |
| 243 | + end) |
| 244 | + end |
| 245 | +end) |
0 commit comments