-- Path of Building -- -- Module: Calc Offence -- Performs offence calculations. -- local calcs = ... local pairs = pairs local ipairs = ipairs local unpack = unpack local t_insert = table.insert local m_floor = math.floor local m_min = math.min local m_max = math.max local m_sqrt = math.sqrt local m_pi = math.pi local bor = bit.bor local band = bit.band local bnot = bit.bnot local s_format = string.format local tempTable1 = { } local tempTable2 = { } local tempTable3 = { } local isElemental = { Fire = true, Cold = true, Lightning = true } -- List of all damage types, ordered according to the conversion sequence local dmgTypeList = {"Physical", "Lightning", "Cold", "Fire", "Chaos"} -- Calculate min/max damage of a hit for the given damage type local function calcHitDamage(activeSkill, source, cfg, breakdown, damageType, ...) local skillModList = activeSkill.skillModList local damageTypeMin = damageType.."Min" local damageTypeMax = damageType.."Max" -- Calculate base values local damageEffectiveness = activeSkill.activeEffect.grantedEffectLevel.damageEffectiveness or activeSkill.skillData.damageEffectiveness or 1 local addedMin = skillModList:Sum("BASE", cfg, damageTypeMin) local addedMax = skillModList:Sum("BASE", cfg, damageTypeMax) local baseMin = (source[damageTypeMin] or 0) + addedMin * damageEffectiveness local baseMax = (source[damageTypeMax] or 0) + addedMax * damageEffectiveness if breakdown and not (...) and baseMin ~= 0 and baseMax ~= 0 then t_insert(breakdown, "Base damage:") local plus = "" if (source[damageTypeMin] or 0) ~= 0 or (source[damageTypeMax] or 0) ~= 0 then t_insert(breakdown, s_format("%d to %d ^8(base damage from %s)", source[damageTypeMin], source[damageTypeMax], source.type and "weapon" or "skill")) plus = "+ " end if addedMin ~= 0 or addedMax ~= 0 then if damageEffectiveness ~= 1 then t_insert(breakdown, s_format("%s(%d to %d) x %.2f ^8(added damage multiplied by damage effectiveness)", plus, addedMin, addedMax, damageEffectiveness)) else t_insert(breakdown, s_format("%s%d to %d ^8(added damage)", plus, addedMin, addedMax)) end end t_insert(breakdown, s_format("= %.1f to %.1f", baseMin, baseMax)) end -- Calculate conversions local addMin, addMax = 0, 0 local conversionTable = activeSkill.conversionTable for _, otherType in ipairs(dmgTypeList) do if otherType == damageType then -- Damage can only be converted from damage types that preceed this one in the conversion sequence, so stop here break end local convMult = conversionTable[otherType][damageType] if convMult > 0 then -- Damage is being converted/gained from the other damage type local min, max = calcHitDamage(activeSkill, source, cfg, breakdown, otherType, damageType, ...) addMin = addMin + min * convMult addMax = addMax + max * convMult end end if addMin ~= 0 and addMax ~= 0 then addMin = round(addMin) addMax = round(addMax) end if baseMin == 0 and baseMax == 0 then -- No base damage for this type, don't need to calculate modifiers if breakdown and (addMin ~= 0 or addMax ~= 0) then t_insert(breakdown.damageTypes, { source = damageType, convSrc = (addMin ~= 0 or addMax ~= 0) and (addMin .. " to " .. addMax), total = addMin .. " to " .. addMax, convDst = (...) and s_format("%d%% to %s", conversionTable[damageType][...] * 100, ...), }) end return addMin, addMax end -- Build lists of applicable modifier names local addElemental = isElemental[damageType] local modNames = { damageType.."Damage", "Damage" } for i = 1, select('#', ...) do local dstElem = select(i, ...) -- Add modifiers for damage types to which this damage is being converted addElemental = addElemental or isElemental[dstElem] t_insert(modNames, dstElem.."Damage") end if addElemental then -- Damage is elemental or is being converted to elemental damage, add global elemental modifiers t_insert(modNames, "ElementalDamage") end -- Combine modifiers local inc = 1 + skillModList:Sum("INC", cfg, unpack(modNames)) / 100 local more = m_floor(skillModList:More(cfg, unpack(modNames)) * 100 + 0.50000001) / 100 if breakdown then t_insert(breakdown.damageTypes, { source = damageType, base = baseMin .. " to " .. baseMax, inc = (inc ~= 1 and "x "..inc), more = (more ~= 1 and "x "..more), convSrc = (addMin ~= 0 or addMax ~= 0) and (addMin .. " to " .. addMax), total = (round(baseMin * inc * more) + addMin) .. " to " .. (round(baseMax * inc * more) + addMax), convDst = (...) and s_format("%d%% to %s", conversionTable[damageType][...] * 100, ...), }) end return (round(baseMin * inc * more) + addMin), (round(baseMax * inc * more) + addMax) end -- Performs all offensive calculations function calcs.offence(env, actor, activeSkill) local enemyDB = actor.enemy.modDB local output = actor.output local breakdown = actor.breakdown local skillModList = activeSkill.skillModList local skillData = activeSkill.skillData local skillFlags = activeSkill.skillFlags local skillCfg = activeSkill.skillCfg if skillData.showAverage then skillFlags.showAverage = true else skillFlags.notAverage = true end if skillFlags.disable then -- Skill is disabled output.CombinedDPS = 0 return end -- Update skill data for _, value in ipairs(skillModList:List(skillCfg, "SkillData")) do if value.merge == "MAX" then skillData[value.key] = m_max(value.value, skillData[value.key] or 0) else skillData[value.key] = value.value end end skillCfg.skillCond["SkillIsTriggered"] = skillData.triggered -- Add addition stat bonuses if skillModList:Flag(nil, "IronGrip") then skillModList:NewMod("PhysicalDamage", "INC", actor.strDmgBonus, "Strength", bor(ModFlag.Attack, ModFlag.Projectile)) end if skillModList:Flag(nil, "IronWill") then skillModList:NewMod("Damage", "INC", actor.strDmgBonus, "Strength", ModFlag.Spell) end if skillModList:Flag(nil, "MinionDamageAppliesToPlayer") then -- Minion Damage conversion from The Scourge for _, value in ipairs(skillModList:List(skillCfg, "MinionModifier")) do if value.mod.name == "Damage" and value.mod.type == "INC" then skillModList:AddMod(value.mod) end end end if skillModList:Flag(nil, "SpellDamageAppliesToAttacks") then -- Spell Damage conversion from Crown of Eyes for i, value in ipairs(skillModList:Tabulate("INC", { flags = ModFlag.Spell }, "Damage")) do local mod = value.mod if band(mod.flags, ModFlag.Spell) ~= 0 then skillModList:NewMod("Damage", "INC", mod.value, mod.source, bor(band(mod.flags, bnot(ModFlag.Spell)), ModFlag.Attack), mod.keywordFlags, unpack(mod)) end end end if skillModList:Flag(nil, "ClawDamageAppliesToUnarmed") then -- Claw Damage conversion from Rigwald's Curse for i, value in ipairs(skillModList:Tabulate("INC", { flags = ModFlag.Claw }, "PhysicalDamage")) do local mod = value.mod if band(mod.flags, ModFlag.Claw) ~= 0 then skillModList:NewMod("PhysicalDamage", mod.type, mod.value, mod.source, bor(band(mod.flags, bnot(ModFlag.Claw)), ModFlag.Unarmed), mod.keywordFlags, unpack(mod)) end end end if skillModList:Flag(nil, "ClawAttackSpeedAppliesToUnarmed") then -- Claw Attack Speed conversion from Rigwald's Curse for i, value in ipairs(skillModList:Tabulate("INC", { flags = bor(ModFlag.Claw, ModFlag.Attack) }, "Speed")) do local mod = value.mod if band(mod.flags, ModFlag.Claw) ~= 0 and band(mod.flags, ModFlag.Attack) ~= 0 then skillModList:NewMod("Speed", mod.type, mod.value, mod.source, bor(band(mod.flags, bnot(ModFlag.Claw)), ModFlag.Unarmed), mod.keywordFlags, unpack(mod)) end end end if skillModList:Flag(nil, "ClawCritChanceAppliesToUnarmed") then -- Claw Crit Chance conversion from Rigwald's Curse for i, value in ipairs(skillModList:Tabulate("INC", { flags = ModFlag.Claw }, "Speed")) do local mod = value.mod if band(mod.flags, ModFlag.Claw) ~= 0 then skillModList:NewMod("CritChance", mod.type, mod.value, mod.source, bor(band(mod.flags, bnot(ModFlag.Claw)), ModFlag.Unarmed), mod.keywordFlags, unpack(mod)) end end end local isAttack = skillFlags.attack -- Calculate skill type stats if skillFlags.minion then if activeSkill.minion and activeSkill.minion.minionData.limit then output.ActiveMinionLimit = m_floor(calcLib.val(skillModList, activeSkill.minion.minionData.limit, skillCfg)) end end if skillFlags.chaining then output.ChainMax = skillModList:Sum("BASE", skillCfg, "ChainCountMax") output.Chain = m_min(output.ChainMax, skillModList:Sum("BASE", skillCfg, "ChainCount")) output.ChainRemaining = m_max(0, output.ChainMax - output.Chain) end if skillFlags.projectile then if skillModList:Flag(nil, "PointBlank") then skillModList:NewMod("Damage", "MORE", 50, "Point Blank", bor(ModFlag.Attack, ModFlag.Projectile), { type = "DistanceRamp", ramp = {{10,1},{35,0},{150,-1}} }) end output.ProjectileCount = skillModList:Sum("BASE", skillCfg, "ProjectileCount") output.PierceChance = m_min(100, skillModList:Sum("BASE", skillCfg, "PierceChance")) output.ProjectileSpeedMod = calcLib.mod(skillModList, skillCfg, "ProjectileSpeed") if breakdown then breakdown.ProjectileSpeedMod = breakdown.mod(skillCfg, "ProjectileSpeed") end end if skillFlags.melee then if skillFlags.weapon1Attack then actor.weaponRange1 = (actor.weaponData1.range and actor.weaponData1.range + skillModList:Sum("BASE", skillCfg, "MeleeWeaponRange")) or (4 + skillModList:Sum("BASE", skillCfg, "UnarmedRange")) end if skillFlags.weapon2Attack then actor.weaponRange2 = (actor.weaponData2.range and actor.weaponData2.range + skillModList:Sum("BASE", skillCfg, "MeleeWeaponRange")) or (4 + skillModList:Sum("BASE", skillCfg, "UnarmedRange")) end if activeSkill.skillTypes[SkillType.MeleeSingleTarget] then local range = 100 if skillFlags.weapon1Attack then range = m_min(range, actor.weaponRange1) end if skillFlags.weapon2Attack then range = m_min(range, actor.weaponRange2) end output.WeaponRange = range + 2 if breakdown then breakdown.WeaponRange = { radius = output.WeaponRange } end end end if skillFlags.area or skillData.radius then output.AreaOfEffectMod = calcLib.mod(skillModList, skillCfg, "AreaOfEffect") if skillData.radiusIsWeaponRange then local range = 0 if skillFlags.weapon1Attack then range = m_max(range, actor.weaponRange1) end if skillFlags.weapon2Attack then range = m_max(range, actor.weaponRange2) end skillData.radius = range + 2 end if skillData.radius then skillFlags.area = true local baseRadius = skillData.radius + (skillData.radiusExtra or 0) + skillModList:Sum("BASE", skillCfg, "AreaOfEffect") output.AreaOfEffectRadius = m_floor(baseRadius * m_sqrt(output.AreaOfEffectMod)) if breakdown then breakdown.AreaOfEffectRadius = breakdown.area(baseRadius, output.AreaOfEffectMod, output.AreaOfEffectRadius) end if skillData.radiusSecondary then baseRadius = skillData.radiusSecondary + (skillData.radiusExtra or 0) output.AreaOfEffectRadiusSecondary = m_floor(baseRadius * m_sqrt(output.AreaOfEffectMod)) if breakdown then breakdown.AreaOfEffectRadiusSecondary = breakdown.area(baseRadius, output.AreaOfEffectMod, output.AreaOfEffectRadiusSecondary) end end end if breakdown then breakdown.AreaOfEffectMod = breakdown.mod(skillCfg, "AreaOfEffect") end end if skillFlags.trap then local baseSpeed = 1 / skillModList:Sum("BASE", skillCfg, "TrapThrowingTime") output.TrapThrowingSpeed = baseSpeed * calcLib.mod(skillModList, skillCfg, "TrapThrowingSpeed") * output.ActionSpeedMod output.TrapThrowingTime = 1 / output.TrapThrowingSpeed if breakdown then breakdown.TrapThrowingTime = { } breakdown.multiChain(breakdown.TrapThrowingTime, { label = "Throwing speed:", base = s_format("%.2f ^8(base throwing speed)", baseSpeed), { "%.2f ^8(increased/reduced throwing speed)", 1 + skillModList:Sum("INC", skillCfg, "TrapThrowingSpeed") / 100 }, { "%.2f ^8(more/less throwing speed)", skillModList:More(skillCfg, "TrapThrowingSpeed") }, { "%.2f ^8(action speed modifier)", output.ActionSpeedMod }, total = s_format("= %.2f ^8per second", output.TrapThrowingSpeed), }) end output.ActiveTrapLimit = skillModList:Sum("BASE", skillCfg, "ActiveTrapLimit") output.TrapCooldown = (skillData.trapCooldown or skillData.cooldown or 4) / calcLib.mod(skillModList, skillCfg, "CooldownRecovery") if breakdown then breakdown.TrapCooldown = { s_format("%.2fs ^8(base)", skillData.trapCooldown or skillData.cooldown or 4), s_format("/ %.2f ^8(increased/reduced cooldown recovery)", 1 + skillModList:Sum("INC", skillCfg, "CooldownRecovery") / 100), s_format("= %.2fs", output.TrapCooldown) } end local areaMod = calcLib.mod(skillModList, skillCfg, "TrapTriggerAreaOfEffect") output.TrapTriggerRadius = 10 * m_sqrt(areaMod) if breakdown then breakdown.TrapTriggerRadius = breakdown.area(10, areaMod, output.TrapTriggerRadius) end elseif skillData.cooldown then output.Cooldown = skillData.cooldown / calcLib.mod(skillModList, skillCfg, "CooldownRecovery") if breakdown then breakdown.Cooldown = { s_format("%.2fs ^8(base)", skillData.cooldown), s_format("/ %.2f ^8(increased/reduced cooldown recovery)", 1 + skillModList:Sum("INC", skillCfg, "CooldownRecovery") / 100), s_format("= %.2fs", output.Cooldown) } end end if skillFlags.mine then local baseSpeed = 1 / skillModList:Sum("BASE", skillCfg, "MineLayingTime") output.MineLayingSpeed = baseSpeed * calcLib.mod(skillModList, skillCfg, "MineLayingSpeed") * output.ActionSpeedMod output.MineLayingTime = 1 / output.MineLayingSpeed if breakdown then breakdown.MineLayingTime = { } breakdown.multiChain(breakdown.MineLayingTime, { label = "Laying speed:", base = s_format("%.2f ^8(base laying speed)", baseSpeed), { "%.2f ^8(increased/reduced laying speed)", 1 + skillModList:Sum("INC", skillCfg, "MineLayingSpeed") / 100 }, { "%.2f ^8(more/less laying speed)", skillModList:More(skillCfg, "MineLayingSpeed") }, { "%.2f ^8(action speed modifier)", output.ActionSpeedMod }, total = s_format("= %.2f ^8per second", output.MineLayingSpeed), }) end output.ActiveMineLimit = skillModList:Sum("BASE", skillCfg, "ActiveMineLimit") local areaMod = calcLib.mod(skillModList, skillCfg, "MineDetonationAreaOfEffect") output.MineDetonationRadius = 60 * m_sqrt(areaMod) if breakdown then breakdown.MineDetonationRadius = breakdown.area(60, areaMod, output.MineDetonationRadius) end end if skillFlags.totem then local baseSpeed = 1 / skillModList:Sum("BASE", skillCfg, "TotemPlacementTime") output.TotemPlacementSpeed = baseSpeed * calcLib.mod(skillModList, skillCfg, "TotemPlacementSpeed") * output.ActionSpeedMod output.TotemPlacementTime = 1 / output.TotemPlacementSpeed if breakdown then breakdown.TotemPlacementTime = { } breakdown.multiChain(breakdown.TotemPlacementTime, { label = "Placement speed:", base = s_format("%.2f ^8(base placement speed)", baseSpeed), { "%.2f ^8(increased/reduced placement speed)", 1 + skillModList:Sum("INC", skillCfg, "TotemPlacementSpeed") / 100 }, { "%.2f ^8(more/less placement speed)", skillModList:More(skillCfg, "TotemPlacementSpeed") }, { "%.2f ^8(action speed modifier)", output.ActionSpeedMod }, total = s_format("= %.2f ^8per second", output.TotemPlacementSpeed), }) end output.ActiveTotemLimit = skillModList:Sum("BASE", skillCfg, "ActiveTotemLimit") output.TotemLifeMod = calcLib.mod(skillModList, skillCfg, "TotemLife") output.TotemLife = round(m_floor(env.data.monsterAllyLifeTable[skillData.totemLevel] * env.data.totemLifeMult[activeSkill.skillTotemId]) * output.TotemLifeMod) if breakdown then breakdown.TotemLifeMod = breakdown.mod(skillCfg, "TotemLife") breakdown.TotemLife = { "Totem level: "..skillData.totemLevel, env.data.monsterAllyLifeTable[skillData.totemLevel].." ^8(base life for a level "..skillData.totemLevel.." monster)", "x "..env.data.totemLifeMult[activeSkill.skillTotemId].." ^8(life multiplier for this totem type)", "x "..output.TotemLifeMod.." ^8(totem life modifier)", "= "..output.TotemLife, } end end -- Skill duration local debuffDurationMult if env.mode_effective then debuffDurationMult = 1 / calcLib.mod(enemyDB, skillCfg, "BuffExpireFaster") else debuffDurationMult = 1 end do output.DurationMod = calcLib.mod(skillModList, skillCfg, "Duration") if breakdown then breakdown.DurationMod = breakdown.mod(skillCfg, "Duration") end local durationBase = skillData.duration or 0 if durationBase > 0 then output.Duration = durationBase * output.DurationMod if skillData.debuff then output.Duration = output.Duration * debuffDurationMult end if breakdown and output.Duration ~= durationBase then breakdown.Duration = { s_format("%.2fs ^8(base)", durationBase), } if output.DurationMod ~= 1 then t_insert(breakdown.Duration, s_format("x %.2f ^8(duration modifier)", output.DurationMod)) end if skillData.debuff and debuffDurationMult ~= 1 then t_insert(breakdown.Duration, s_format("/ %.2f ^8(debuff expires slower/faster)", 1 / debuffDurationMult)) end t_insert(breakdown.Duration, s_format("= %.2fs", output.Duration)) end end end -- Run skill setup function do local setupFunc = activeSkill.activeEffect.grantedEffect.setupFunc if setupFunc then setupFunc(activeSkill, output) end end -- Cache global damage disabling flags local canDeal = { } for _, damageType in pairs(dmgTypeList) do canDeal[damageType] = not skillModList:Flag(skillCfg, "DealNo"..damageType) end -- Calculate damage conversion percentages activeSkill.conversionTable = wipeTable(activeSkill.conversionTable) for damageTypeIndex = 1, 4 do local damageType = dmgTypeList[damageTypeIndex] local globalConv = wipeTable(tempTable1) local skillConv = wipeTable(tempTable2) local add = wipeTable(tempTable3) local globalTotal, skillTotal = 0, 0 for otherTypeIndex = damageTypeIndex + 1, 5 do -- For all possible destination types, check for global and skill conversions otherType = dmgTypeList[otherTypeIndex] globalConv[otherType] = skillModList:Sum("BASE", skillCfg, damageType.."DamageConvertTo"..otherType, isElemental[damageType] and "ElementalDamageConvertTo"..otherType or nil) globalTotal = globalTotal + globalConv[otherType] skillConv[otherType] = skillModList:Sum("BASE", skillCfg, "Skill"..damageType.."DamageConvertTo"..otherType) skillTotal = skillTotal + skillConv[otherType] add[otherType] = skillModList:Sum("BASE", skillCfg, damageType.."DamageGainAs"..otherType, isElemental[damageType] and "ElementalDamageGainAs"..otherType or nil) end if skillTotal > 100 then -- Skill conversion exceeds 100%, scale it down and remove non-skill conversions local factor = 100 / skillTotal for type, val in pairs(skillConv) do -- The game currently doesn't scale this down even though it is supposed to --skillConv[type] = val * factor end for type, val in pairs(globalConv) do globalConv[type] = 0 end elseif globalTotal + skillTotal > 100 then -- Conversion exceeds 100%, scale down non-skill conversions local factor = (100 - skillTotal) / globalTotal for type, val in pairs(globalConv) do globalConv[type] = val * factor end globalTotal = globalTotal * factor end local dmgTable = { } for type, val in pairs(globalConv) do dmgTable[type] = (globalConv[type] + skillConv[type] + add[type]) / 100 end dmgTable.mult = 1 - m_min((globalTotal + skillTotal) / 100, 1) activeSkill.conversionTable[damageType] = dmgTable end activeSkill.conversionTable["Chaos"] = { mult = 1 } -- Calculate mana cost (may be slightly off due to rounding differences) do local more = m_floor(skillModList:More(skillCfg, "ManaCost") * 100 + 0.0001) / 100 local inc = skillModList:Sum("INC", skillCfg, "ManaCost") local base = skillModList:Sum("BASE", skillCfg, "ManaCost") local manaCost = activeSkill.activeEffect.grantedEffectLevel.manaCost or 0 output.ManaCost = m_floor(m_max(0, manaCost * more * (1 + inc / 100) + base)) if activeSkill.skillTypes[SkillType.ManaCostPercent] and skillFlags.totem then output.ManaCost = m_floor(output.Mana * output.ManaCost / 100) end if breakdown and output.ManaCost ~= manaCost then breakdown.ManaCost = { s_format("%d ^8(base mana cost)", manaCost) } if more ~= 1 then t_insert(breakdown.ManaCost, s_format("x %.2f ^8(mana cost multiplier)", more)) end if inc ~= 0 then t_insert(breakdown.ManaCost, s_format("x %.2f ^8(increased/reduced mana cost)", 1 + inc/100)) end if base ~= 0 then t_insert(breakdown.ManaCost, s_format("- %d ^8(- mana cost)", -base)) end t_insert(breakdown.ManaCost, s_format("= %d", output.ManaCost)) end end -- Configure damage passes local passList = { } if isAttack then output.MainHand = { } output.OffHand = { } if skillFlags.weapon1Attack then if breakdown then breakdown.MainHand = LoadModule(calcs.breakdownModule, skillModList, output.MainHand) end activeSkill.weapon1Cfg.skillStats = output.MainHand t_insert(passList, { label = "Main Hand", source = actor.weaponData1, cfg = activeSkill.weapon1Cfg, output = output.MainHand, breakdown = breakdown and breakdown.MainHand, }) end if skillFlags.weapon2Attack then if breakdown then breakdown.OffHand = LoadModule(calcs.breakdownModule, skillModList, output.OffHand) end activeSkill.weapon2Cfg.skillStats = output.OffHand t_insert(passList, { label = "Off Hand", source = actor.weaponData2, cfg = activeSkill.weapon2Cfg, output = output.OffHand, breakdown = breakdown and breakdown.OffHand, }) end else t_insert(passList, { label = "Skill", source = skillData, cfg = skillCfg, output = output, breakdown = breakdown, }) end local function combineStat(stat, mode, ...) -- Combine stats from Main Hand and Off Hand according to the mode if mode == "OR" or not skillFlags.bothWeaponAttack then output[stat] = output.MainHand[stat] or output.OffHand[stat] elseif mode == "ADD" then output[stat] = (output.MainHand[stat] or 0) + (output.OffHand[stat] or 0) elseif mode == "AVERAGE" then output[stat] = ((output.MainHand[stat] or 0) + (output.OffHand[stat] or 0)) / 2 elseif mode == "CHANCE" then if output.MainHand[stat] and output.OffHand[stat] then local mainChance = output.MainHand[...] * output.MainHand.HitChance local offChance = output.OffHand[...] * output.OffHand.HitChance local mainPortion = mainChance / (mainChance + offChance) local offPortion = offChance / (mainChance + offChance) output[stat] = output.MainHand[stat] * mainPortion + output.OffHand[stat] * offPortion if breakdown then if not breakdown[stat] then breakdown[stat] = { } end t_insert(breakdown[stat], "Contribution from Main Hand:") t_insert(breakdown[stat], s_format("%.1f", output.MainHand[stat])) t_insert(breakdown[stat], s_format("x %.3f ^8(portion of instances created by main hand)", mainPortion)) t_insert(breakdown[stat], s_format("= %.1f", output.MainHand[stat] * mainPortion)) t_insert(breakdown[stat], "Contribution from Off Hand:") t_insert(breakdown[stat], s_format("%.1f", output.OffHand[stat])) t_insert(breakdown[stat], s_format("x %.3f ^8(portion of instances created by off hand)", offPortion)) t_insert(breakdown[stat], s_format("= %.1f", output.OffHand[stat] * offPortion)) t_insert(breakdown[stat], "Total:") t_insert(breakdown[stat], s_format("%.1f + %.1f", output.MainHand[stat] * mainPortion, output.OffHand[stat] * offPortion)) t_insert(breakdown[stat], s_format("= %.1f", output[stat])) end else output[stat] = output.MainHand[stat] or output.OffHand[stat] end elseif mode == "DPS" then output[stat] = (output.MainHand[stat] or 0) + (output.OffHand[stat] or 0) if not skillData.doubleHitsWhenDualWielding then output[stat] = output[stat] / 2 end end end for _, pass in ipairs(passList) do local globalOutput, globalBreakdown = output, breakdown local source, output, cfg, breakdown = pass.source, pass.output, pass.cfg, pass.breakdown -- Calculate hit chance output.Accuracy = calcLib.val(skillModList, "Accuracy", cfg) if breakdown then breakdown.Accuracy = breakdown.simple(nil, cfg, output.Accuracy, "Accuracy") end if not isAttack or skillModList:Flag(cfg, "CannotBeEvaded") or skillData.cannotBeEvaded then output.HitChance = 100 else local enemyEvasion = round(calcLib.val(enemyDB, "Evasion")) output.HitChance = calcs.hitChance(enemyEvasion, output.Accuracy) if breakdown then breakdown.HitChance = { "Enemy level: "..env.enemyLevel..(env.configInput.enemyLevel and " ^8(overridden from the Configuration tab" or " ^8(can be overridden in the Configuration tab)"), "Average enemy evasion: "..enemyEvasion, "Approximate hit chance: "..output.HitChance.."%", } end end -- Calculate attack/cast speed if skillData.timeOverride then output.Time = skillData.timeOverride output.Speed = 1 / output.Time else local baseSpeed if isAttack then if skillData.castTimeOverridesAttackTime then -- Skill is overriding weapon attack speed baseSpeed = 1 / skillData.castTime * (1 + (source.AttackSpeedInc or 0) / 100) else baseSpeed = source.AttackRate or 1 end else baseSpeed = 1 / (skillData.castTime or 1) end local inc = skillModList:Sum("INC", cfg, "Speed") local more = skillModList:More(cfg, "Speed") output.Speed = baseSpeed * round((1 + inc/100) * more, 2) if skillData.attackRateCap then output.Speed = m_min(output.Speed, skillData.attackRateCap) end if skillFlags.selfCast then -- Self-cast skill; apply action speed output.Speed = output.Speed * globalOutput.ActionSpeedMod end if breakdown then breakdown.Speed = { } breakdown.multiChain(breakdown.Speed, { base = s_format("%.2f ^8(base)", baseSpeed), { "%.2f ^8(increased/reduced)", 1 + inc/100 }, { "%.2f ^8(more/less)", more }, { "%.2f ^8(action speed modifier)", skillFlags.selfCast and globalOutput.ActionSpeedMod or 1 }, total = s_format("= %.2f ^8per second", output.Speed) }) end if output.Speed == 0 then output.Time = 0 else output.Time = 1 / output.Speed end end if skillData.hitTimeOverride then output.HitTime = skillData.hitTimeOverride output.HitSpeed = 1 / output.HitTime end end if isAttack then -- Combine hit chance and attack speed combineStat("HitChance", "AVERAGE") combineStat("Speed", "AVERAGE") if output.Speed == 0 then output.Time = 0 else output.Time = 1 / output.Speed end if skillFlags.bothWeaponAttack then if breakdown then breakdown.Speed = { "Both weapons:", s_format("(%.2f + %.2f) / 2", output.MainHand.Speed, output.OffHand.Speed), s_format("= %.2f", output.Speed), } end end end for _, pass in ipairs(passList) do local globalOutput, globalBreakdown = output, breakdown local source, output, cfg, breakdown = pass.source, pass.output, pass.cfg, pass.breakdown -- Calculate crit chance, crit multiplier, and their combined effect if skillModList:Flag(nil, "NeverCrit") then output.PreEffectiveCritChance = 0 output.CritChance = 0 output.CritMultiplier = 0 output.CritEffect = 1 else local baseCrit = source.CritChance or 0 if baseCrit == 100 then output.PreEffectiveCritChance = 100 output.CritChance = 100 else local base = skillModList:Sum("BASE", cfg, "CritChance") local inc = skillModList:Sum("INC", cfg, "CritChance") local more = skillModList:More(cfg, "CritChance") local enemyExtra = env.mode_effective and enemyDB:Sum("BASE", nil, "SelfExtraCritChance") or 0 output.CritChance = (baseCrit + base) * (1 + inc / 100) * more local preCapCritChance = output.CritChance output.CritChance = m_min(output.CritChance, 95) if (baseCrit + base) > 0 then output.CritChance = m_max(output.CritChance, 5) end output.PreEffectiveCritChance = output.CritChance if enemyExtra ~= 0 then output.CritChance = m_min(output.CritChance + enemyExtra, 100) end local preLuckyCritChance = output.CritChance if env.mode_effective and skillModList:Flag(cfg, "CritChanceLucky") then output.CritChance = (1 - (1 - output.CritChance / 100) ^ 2) * 100 end local preHitCheckCritChance = output.CritChance if env.mode_effective then output.CritChance = output.CritChance * output.HitChance / 100 end if breakdown and output.CritChance ~= baseCrit then breakdown.CritChance = { } if base ~= 0 then t_insert(breakdown.CritChance, s_format("(%g + %g) ^8(base)", baseCrit, base)) else t_insert(breakdown.CritChance, s_format("%g ^8(base)", baseCrit + base)) end if inc ~= 0 then t_insert(breakdown.CritChance, s_format("x %.2f", 1 + inc/100).." ^8(increased/reduced)") end if more ~= 1 then t_insert(breakdown.CritChance, s_format("x %.2f", more).." ^8(more/less)") end t_insert(breakdown.CritChance, s_format("= %.2f%% ^8(crit chance)", output.PreEffectiveCritChance)) if preCapCritChance > 95 then local overCap = preCapCritChance - 95 t_insert(breakdown.CritChance, s_format("Crit is overcapped by %.2f%% (%d%% increased Critical Strike Chance)", overCap, overCap / more / (baseCrit + base) * 100)) end if enemyExtra ~= 0 then t_insert(breakdown.CritChance, s_format("+ %g ^8(extra chance for enemy to be crit)", enemyExtra)) t_insert(breakdown.CritChance, s_format("= %.2f%% ^8(chance to crit against enemy)", preLuckyCritChance)) end if env.mode_effective and skillModList:Flag(cfg, "CritChanceLucky") then t_insert(breakdown.CritChance, "Crit Chance is Lucky:") t_insert(breakdown.CritChance, s_format("1 - (1 - %.4f) x (1 - %.4f)", preLuckyCritChance / 100, preLuckyCritChance / 100)) t_insert(breakdown.CritChance, s_format("= %.2f%%", preHitCheckCritChance)) end if env.mode_effective and output.HitChance < 100 then t_insert(breakdown.CritChance, "Crit confirmation roll:") t_insert(breakdown.CritChance, s_format("%.2f%%", preHitCheckCritChance)) t_insert(breakdown.CritChance, s_format("x %.2f ^8(chance to hit)", output.HitChance / 100)) t_insert(breakdown.CritChance, s_format("= %.2f%%", output.CritChance)) end end end if skillModList:Flag(cfg, "NoCritMultiplier") then output.CritMultiplier = 1 else local extraDamage = skillModList:Sum("BASE", cfg, "CritMultiplier") / 100 if env.mode_effective then local enemyInc = 1 + enemyDB:Sum("INC", nil, "SelfCritMultiplier") / 100 extraDamage = round(extraDamage * enemyInc, 2) if breakdown and enemyInc ~= 1 then breakdown.CritMultiplier = { s_format("%d%% ^8(additional extra damage)", skillModList:Sum("BASE", cfg, "CritMultiplier") / 100), s_format("x %.2f ^8(increased/reduced extra crit damage taken by enemy)", enemyInc), s_format("= %d%% ^8(extra crit damage)", extraDamage * 100), } end end output.CritMultiplier = 1 + m_max(0, extraDamage) end output.CritEffect = 1 - output.CritChance / 100 + output.CritChance / 100 * output.CritMultiplier if breakdown and output.CritEffect ~= 1 then breakdown.CritEffect = { s_format("(1 - %.4f) ^8(portion of damage from non-crits)", output.CritChance/100), s_format("+ (%.4f x %g) ^8(portion of damage from crits)", output.CritChance/100, output.CritMultiplier), s_format("= %.3f", output.CritEffect), } end end -- Calculate hit damage for each damage type local totalHitMin, totalHitMax = 0, 0 local totalCritMin, totalCritMax = 0, 0 output.LifeLeech = 0 output.LifeLeechInstant = 0 output.ManaLeech = 0 output.ManaLeechInstant = 0 for pass = 1, 2 do -- Pass 1 is critical strike damage, pass 2 is non-critical strike cfg.skillCond["CriticalStrike"] = (pass == 1) local lifeLeechTotal = 0 local manaLeechTotal = 0 local noLifeLeech = skillModList:Flag(cfg, "CannotLeechLife") or enemyDB:Flag(nil, "CannotLeechLifeFromSelf") local noManaLeech = skillModList:Flag(cfg, "CannotLeechMana") or enemyDB:Flag(nil, "CannotLeechManaFromSelf") for _, damageType in ipairs(dmgTypeList) do local min, max if skillFlags.hit and canDeal[damageType] then if breakdown then breakdown[damageType] = { damageTypes = { } } end min, max = calcHitDamage(activeSkill, source, cfg, breakdown and breakdown[damageType], damageType) local convMult = activeSkill.conversionTable[damageType].mult local doubleChance = m_min(skillModList:Sum("BASE", cfg, "DoubleDamageChance"), 100) if breakdown then t_insert(breakdown[damageType], "Hit damage:") t_insert(breakdown[damageType], s_format("%d to %d ^8(total damage)", min, max)) if convMult ~= 1 then t_insert(breakdown[damageType], s_format("x %g ^8(%g%% converted to other damage types)", convMult, (1-convMult)*100)) end if doubleChance > 0 then t_insert(breakdown[damageType], s_format("x %.2f ^8(chance to deal double damage)", 1 + doubleChance / 100)) end end min = min * convMult max = max * convMult if doubleChance > 0 then min = min * (1 + doubleChance / 100) max = max * (1 + doubleChance / 100) end if pass == 1 then -- Apply crit multiplier min = min * output.CritMultiplier max = max * output.CritMultiplier end if (min ~= 0 or max ~= 0) and env.mode_effective then -- Apply enemy resistances and damage taken modifiers local resist = 0 local pen = 0 local taken = enemyDB:Sum("INC", nil, "DamageTaken", damageType.."DamageTaken") if damageType == "Physical" then resist = enemyDB:Sum("BASE", nil, "PhysicalDamageReduction") else resist = enemyDB:Sum("BASE", nil, damageType.."Resist") if isElemental[damageType] then resist = resist + enemyDB:Sum("BASE", nil, "ElementalResist") pen = skillModList:Sum("BASE", cfg, damageType.."Penetration", "ElementalPenetration") taken = taken + enemyDB:Sum("INC", nil, "ElementalDamageTaken") end resist = m_min(resist, 75) end if skillFlags.projectile then taken = taken + enemyDB:Sum("INC", nil, "ProjectileDamageTaken") end local effMult = (1 + taken / 100) if not isElemental[damageType] or not skillModList:Flag(cfg, "IgnoreElementalResistances") then effMult = effMult * (1 - (resist - pen) / 100) end min = min * effMult max = max * effMult if env.mode == "CALCS" then output[damageType.."EffMult"] = effMult end if breakdown and effMult ~= 1 then t_insert(breakdown[damageType], s_format("x %.3f ^8(effective DPS modifier)", effMult)) breakdown[damageType.."EffMult"] = breakdown.effMult(damageType, resist, pen, taken, effMult) end end if breakdown then t_insert(breakdown[damageType], s_format("= %d to %d", min, max)) end if skillFlags.mine or skillFlags.trap or skillFlags.totem then if not noLifeLeech then local lifeLeech = skillModList:Sum("BASE", cfg, "DamageLifeLeechToPlayer") if lifeLeech > 0 then lifeLeechTotal = lifeLeechTotal + (min + max) / 2 * lifeLeech / 100 end end else if not noLifeLeech then local lifeLeech if skillModList:Flag(nil, "LifeLeechBasedOnChaosDamage") then if damageType == "Chaos" then lifeLeech = skillModList:Sum("BASE", cfg, "DamageLeech", "DamageLifeLeech", "PhysicalDamageLifeLeech", "LightningDamageLifeLeech", "ColdDamageLifeLeech", "FireDamageLifeLeech", "ChaosDamageLifeLeech", "ElementalDamageLifeLeech") + enemyDB:Sum("BASE", nil, "SelfDamageLifeLeech") / 100 else lifeLeech = 0 end else lifeLeech = skillModList:Sum("BASE", cfg, "DamageLeech", "DamageLifeLeech", damageType.."DamageLifeLeech", isElemental[damageType] and "ElementalDamageLifeLeech" or nil) + enemyDB:Sum("BASE", nil, "SelfDamageLifeLeech") / 100 end if lifeLeech > 0 then lifeLeechTotal = lifeLeechTotal + (min + max) / 2 * lifeLeech / 100 end end if not noManaLeech then local manaLeech = skillModList:Sum("BASE", cfg, "DamageLeech", "DamageManaLeech", damageType.."DamageManaLeech", isElemental[damageType] and "ElementalDamageManaLeech" or nil) + enemyDB:Sum("BASE", nil, "SelfDamageManaLeech") / 100 if manaLeech > 0 then manaLeechTotal = manaLeechTotal + (min + max) / 2 * manaLeech / 100 end end end else min, max = 0, 0 if breakdown then breakdown[damageType] = { "You can't deal "..damageType.." damage" } end end if pass == 1 then output[damageType.."CritAverage"] = (min + max) / 2 totalCritMin = totalCritMin + min totalCritMax = totalCritMax + max else if env.mode == "CALCS" then output[damageType.."Min"] = min output[damageType.."Max"] = max end output[damageType.."HitAverage"] = (min + max) / 2 totalHitMin = totalHitMin + min totalHitMax = totalHitMax + max end end if skillData.lifeLeechPerUse then lifeLeechTotal = lifeLeechTotal + skillData.lifeLeechPerUse end if skillData.manaLeechPerUse then manaLeechTotal = manaLeechTotal + skillData.manaLeechPerUse end local portion = (pass == 1) and (output.CritChance / 100) or (1 - output.CritChance / 100) if skillModList:Flag(cfg, "InstantLifeLeech") then output.LifeLeechInstant = output.LifeLeechInstant + lifeLeechTotal * portion else output.LifeLeech = output.LifeLeech + lifeLeechTotal * portion end if skillModList:Flag(cfg, "InstantManaLeech") then output.ManaLeechInstant = output.ManaLeechInstant + manaLeechTotal * portion else output.ManaLeech = output.ManaLeech + manaLeechTotal * portion end end output.TotalMin = totalHitMin output.TotalMax = totalHitMax if skillModList:Flag(skillCfg, "ElementalEquilibrium") and not env.configInput.EEIgnoreHitDamage and (output.FireHitAverage + output.ColdHitAverage + output.LightningHitAverage > 0) then -- Update enemy hit-by-damage-type conditions enemyDB.conditions.HitByFireDamage = output.FireHitAverage > 0 enemyDB.conditions.HitByColdDamage = output.ColdHitAverage > 0 enemyDB.conditions.HitByLightningDamage = output.LightningHitAverage > 0 end if breakdown then -- For each damage type, calculate percentage of total damage for _, damageType in ipairs(dmgTypeList) do if output[damageType.."HitAverage"] > 0 then t_insert(breakdown[damageType], s_format("Portion of total damage: %d%%", output[damageType.."HitAverage"] / (totalHitMin + totalHitMax) * 200)) end end end local hitRate = output.HitChance / 100 * (globalOutput.HitSpeed or globalOutput.Speed) * (skillData.dpsMultiplier or 1) -- Calculate leech local function getLeechInstances(amount, total) if total == 0 then return 0, 0 end local duration = amount / total / 0.02 return duration, duration * hitRate end output.LifeLeechDuration, output.LifeLeechInstances = getLeechInstances(output.LifeLeech, skillModList:Flag(nil, "GhostReaver") and globalOutput.EnergyShield or globalOutput.Life) output.LifeLeechInstantRate = output.LifeLeechInstant * hitRate output.ManaLeechDuration, output.ManaLeechInstances = getLeechInstances(output.ManaLeech, globalOutput.Mana) output.ManaLeechInstantRate = output.ManaLeechInstant * hitRate -- Calculate gain on hit if skillFlags.mine or skillFlags.trap or skillFlags.totem then output.LifeOnHit = 0 output.EnergyShieldOnHit = 0 output.ManaOnHit = 0 else output.LifeOnHit = (skillModList:Sum("BASE", skillCfg, "LifeOnHit") + enemyDB:Sum("BASE", skillCfg, "SelfLifeOnHit")) * globalOutput.LifeRecoveryMod output.EnergyShieldOnHit = (skillModList:Sum("BASE", skillCfg, "EnergyShieldOnHit") + enemyDB:Sum("BASE", skillCfg, "SelfEnergyShieldOnHit")) * globalOutput.EnergyShieldRecoveryMod output.ManaOnHit = (skillModList:Sum("BASE", skillCfg, "ManaOnHit") + enemyDB:Sum("BASE", skillCfg, "SelfManaOnHit")) * globalOutput.ManaRecoveryMod end output.LifeOnHitRate = output.LifeOnHit * hitRate output.EnergyShieldOnHitRate = output.EnergyShieldOnHit * hitRate output.ManaOnHitRate = output.ManaOnHit * hitRate -- Calculate average damage and final DPS output.AverageHit = (totalHitMin + totalHitMax) / 2 * (1 - output.CritChance / 100) + (totalCritMin + totalCritMax) / 2 * output.CritChance / 100 output.AverageDamage = output.AverageHit * output.HitChance / 100 output.TotalDPS = output.AverageDamage * (globalOutput.HitSpeed or globalOutput.Speed) * (skillData.dpsMultiplier or 1) if breakdown then if output.CritEffect ~= 1 then breakdown.AverageHit = { s_format("%.1f x (1 - %.4f) ^8(damage from non-crits)", (totalHitMin + totalHitMax) / 2, output.CritChance / 100), s_format("+ %.1f x %.4f ^8(damage from crits)", (totalCritMin + totalCritMax) / 2, output.CritChance / 100), s_format("= %.1f", output.AverageHit), } end if isAttack then breakdown.AverageDamage = { } t_insert(breakdown.AverageDamage, s_format("%s:", pass.label)) t_insert(breakdown.AverageDamage, s_format("%.1f ^8(average hit)", output.AverageHit)) t_insert(breakdown.AverageDamage, s_format("x %.2f ^8(chance to hit)", output.HitChance / 100)) t_insert(breakdown.AverageDamage, s_format("= %.1f", output.AverageDamage)) end end end if isAttack then -- Combine crit stats, average damage and DPS combineStat("PreEffectiveCritChance", "AVERAGE") combineStat("CritChance", "AVERAGE") combineStat("CritMultiplier", "AVERAGE") combineStat("AverageDamage", "DPS") combineStat("TotalDPS", "DPS") combineStat("LifeLeechDuration", "DPS") combineStat("LifeLeechInstances", "DPS") combineStat("LifeLeechInstant", "DPS") combineStat("LifeLeechInstantRate", "DPS") combineStat("ManaLeechDuration", "DPS") combineStat("ManaLeechInstances", "DPS") combineStat("ManaLeechInstant", "DPS") combineStat("ManaLeechInstantRate", "DPS") combineStat("LifeOnHit", "DPS") combineStat("LifeOnHitRate", "DPS") combineStat("EnergyShieldOnHit", "DPS") combineStat("EnergyShieldOnHitRate", "DPS") combineStat("ManaOnHit", "DPS") combineStat("ManaOnHitRate", "DPS") if skillFlags.bothWeaponAttack then if breakdown then breakdown.AverageDamage = { } t_insert(breakdown.AverageDamage, "Both weapons:") if skillData.doubleHitsWhenDualWielding then t_insert(breakdown.AverageDamage, s_format("%.1f + %.1f ^8(skill hits with both weapons at once)", output.MainHand.AverageDamage, output.OffHand.AverageDamage)) else t_insert(breakdown.AverageDamage, s_format("(%.1f + %.1f) / 2 ^8(skill alternates weapons)", output.MainHand.AverageDamage, output.OffHand.AverageDamage)) end t_insert(breakdown.AverageDamage, s_format("= %.1f", output.AverageDamage)) end end end if env.mode == "CALCS" then if skillData.showAverage then output.DisplayDamage = s_format("%.1f average damage", output.AverageDamage) else output.DisplayDamage = s_format("%.1f DPS", output.TotalDPS) end end if breakdown then if isAttack then breakdown.TotalDPS = { s_format("%.1f ^8(average damage)", output.AverageDamage), output.HitSpeed and s_format("x %.2f ^8(hit rate)", output.HitSpeed) or s_format("x %.2f ^8(attack rate)", output.Speed), } else breakdown.TotalDPS = { s_format("%.1f ^8(average hit)", output.AverageDamage), output.HitSpeed and s_format("x %.2f ^8(hit rate)", output.HitSpeed) or s_format("x %.2f ^8(cast rate)", output.Speed), } end if skillData.dpsMultiplier then t_insert(breakdown.TotalDPS, s_format("x %g ^8(DPS multiplier for this skill)", skillData.dpsMultiplier)) end t_insert(breakdown.TotalDPS, s_format("= %.1f", output.TotalDPS)) end -- Calculate leech rates if skillModList:Flag(nil, "GhostReaver") then output.LifeLeechRate = 0 output.LifeLeechPerHit = 0 output.EnergyShieldLeechInstanceRate = output.EnergyShield * 0.02 * calcLib.mod(skillModList, skillCfg, "LifeLeechRate") output.EnergyShieldLeechRate = output.LifeLeechInstantRate * output.EnergyShieldRecoveryMod + m_min(output.LifeLeechInstances * output.EnergyShieldLeechInstanceRate, output.MaxEnergyShieldLeechRate) * output.EnergyShieldRecoveryRateMod output.EnergyShieldLeechPerHit = output.LifeLeechInstant * output.EnergyShieldRecoveryMod + m_min(output.EnergyShieldLeechInstanceRate, output.MaxEnergyShieldLeechRate) * output.LifeLeechDuration * output.EnergyShieldRecoveryRateMod else output.LifeLeechInstanceRate = output.Life * 0.02 * calcLib.mod(skillModList, skillCfg, "LifeLeechRate") output.LifeLeechRate = output.LifeLeechInstantRate * output.LifeRecoveryMod + m_min(output.LifeLeechInstances * output.LifeLeechInstanceRate, output.MaxLifeLeechRate) * output.LifeRecoveryRateMod output.LifeLeechPerHit = output.LifeLeechInstant * output.LifeRecoveryMod + m_min(output.LifeLeechInstanceRate, output.MaxLifeLeechRate) * output.LifeLeechDuration * output.LifeRecoveryRateMod output.EnergyShieldLeechRate = 0 output.EnergyShieldLeechPerHit = 0 end do output.ManaLeechInstanceRate = output.Mana * 0.02 * calcLib.mod(skillModList, skillCfg, "ManaLeechRate") output.ManaLeechRate = output.ManaLeechInstantRate * output.ManaRecoveryMod + m_min(output.ManaLeechInstances * output.ManaLeechInstanceRate, output.MaxManaLeechRate) * output.ManaRecoveryRateMod output.ManaLeechPerHit = output.ManaLeechInstant * output.ManaRecoveryMod + m_min(output.ManaLeechInstanceRate, output.MaxManaLeechRate) * output.ManaLeechDuration * output.ManaRecoveryRateMod end skillFlags.leechES = output.EnergyShieldLeechRate > 0 skillFlags.leechLife = output.LifeLeechRate > 0 skillFlags.leechMana = output.ManaLeechRate > 0 if skillData.showAverage then output.LifeLeechGainPerHit = output.LifeLeechPerHit + output.LifeOnHit output.EnergyShieldLeechGainPerHit = output.EnergyShieldLeechPerHit + output.EnergyShieldOnHit output.ManaLeechGainPerHit = output.ManaLeechPerHit + output.ManaOnHit else output.LifeLeechGainRate = output.LifeLeechRate + output.LifeOnHitRate output.EnergyShieldLeechGainRate = output.EnergyShieldLeechRate + output.EnergyShieldOnHitRate output.ManaLeechGainRate = output.ManaLeechRate + output.ManaOnHitRate end if breakdown then if skillFlags.leechLife then breakdown.LifeLeech = breakdown.leech(output.LifeLeechInstant, output.LifeLeechInstantRate, output.LifeLeechInstances, output.Life, "LifeLeechRate", output.MaxLifeLeechRate, output.LifeLeechDuration) end if skillFlags.leechES then breakdown.EnergyShieldLeech = breakdown.leech(output.LifeLeechInstant, output.LifeLeechInstantRate, output.LifeLeechInstances, output.EnergyShield, "LifeLeechRate", output.MaxEnergyShieldLeechRate, output.LifeLeechDuration) end if skillFlags.leechMana then breakdown.ManaLeech = breakdown.leech(output.ManaLeechInstant, output.ManaLeechInstantRate, output.ManaLeechInstances, output.Mana, "ManaLeechRate", output.MaxManaLeechRate, output.ManaLeechDuration) end end -- Calculate skill DOT components local dotCfg = { skillName = skillCfg.skillName, skillPart = skillCfg.skillPart, skillTypes = skillCfg.skillTypes, slotName = skillCfg.slotName, flags = bor(band(skillCfg.flags, ModFlag.SourceMask), ModFlag.Dot, skillData.dotIsSpell and ModFlag.Spell or 0, skillData.dotIsArea and ModFlag.Area or 0), keywordFlags = skillCfg.keywordFlags } activeSkill.dotCfg = dotCfg output.TotalDot = 0 for _, damageType in ipairs(dmgTypeList) do local baseVal if canDeal[damageType] then baseVal = skillData[damageType.."Dot"] or 0 else baseVal = 0 end if baseVal > 0 then skillFlags.dot = true local effMult = 1 if env.mode_effective then local resist = 0 local taken = enemyDB:Sum("INC", nil, "DamageTaken", "DamageTakenOverTime", damageType.."DamageTaken", damageType.."DamageTakenOverTime") if damageType == "Physical" then resist = enemyDB:Sum("BASE", nil, "PhysicalDamageReduction") else resist = enemyDB:Sum("BASE", nil, damageType.."Resist") if isElemental[damageType] then resist = resist + enemyDB:Sum("BASE", nil, "ElementalResist") taken = taken + enemyDB:Sum("INC", nil, "ElementalDamageTaken") end resist = m_min(resist, 75) end effMult = (1 - resist / 100) * (1 + taken / 100) output[damageType.."DotEffMult"] = effMult if breakdown and effMult ~= 1 then breakdown[damageType.."DotEffMult"] = breakdown.effMult(damageType, resist, 0, taken, effMult) end end local inc = skillModList:Sum("INC", dotCfg, "Damage", damageType.."Damage", isElemental[damageType] and "ElementalDamage" or nil) local more = round(skillModList:More(dotCfg, "Damage", damageType.."Damage", isElemental[damageType] and "ElementalDamage" or nil), 2) local total = baseVal * (1 + inc/100) * more * effMult output[damageType.."Dot"] = total output.TotalDot = output.TotalDot + total if breakdown then breakdown[damageType.."Dot"] = { } breakdown.dot(breakdown[damageType.."Dot"], baseVal, inc, more, nil, nil, effMult, total) end end end skillFlags.bleed = false skillFlags.poison = false skillFlags.ignite = false skillFlags.igniteCanStack = skillModList:Flag(skillCfg, "IgniteCanStack") skillFlags.shock = false skillFlags.freeze = false for _, pass in ipairs(passList) do local globalOutput, globalBreakdown = output, breakdown local source, output, cfg, breakdown = pass.source, pass.output, pass.cfg, pass.breakdown -- Calculate chance to inflict secondary dots/status effects cfg.skillCond["CriticalStrike"] = true if skillModList:Flag(cfg, "CannotBleed") then output.BleedChanceOnCrit = 0 else output.BleedChanceOnCrit = m_min(100, skillModList:Sum("BASE", cfg, "BleedChance")) end output.PoisonChanceOnCrit = m_min(100, skillModList:Sum("BASE", cfg, "PoisonChance")) if skillModList:Flag(cfg, "CannotIgnite") then output.IgniteChanceOnCrit = 0 else output.IgniteChanceOnCrit = 100 end if skillModList:Flag(cfg, "CannotShock") then output.ShockChanceOnCrit = 0 else output.ShockChanceOnCrit = 100 end if skillModList:Flag(cfg, "CannotFreeze") then output.FreezeChanceOnCrit = 0 else output.FreezeChanceOnCrit = 100 end if skillModList:Flag(cfg, "CannotKnockback") then output.KnockbackChanceOnCrit = 0 else output.KnockbackChanceOnCrit = skillModList:Sum("BASE", cfg, "EnemyKnockbackChance") end cfg.skillCond["CriticalStrike"] = false if skillModList:Flag(cfg, "CannotBleed") then output.BleedChanceOnHit = 0 else output.BleedChanceOnHit = m_min(100, skillModList:Sum("BASE", cfg, "BleedChance")) end output.PoisonChanceOnHit = m_min(100, skillModList:Sum("BASE", cfg, "PoisonChance")) output.ChaosPoisonChance = m_min(100, skillModList:Sum("BASE", cfg, "ChaosPoisonChance")) if skillModList:Flag(cfg, "CannotIgnite") then output.IgniteChanceOnHit = 0 else output.IgniteChanceOnHit = m_min(100, skillModList:Sum("BASE", cfg, "EnemyIgniteChance") + enemyDB:Sum("BASE", nil, "SelfIgniteChance")) end if skillModList:Flag(cfg, "CannotShock") then output.ShockChanceOnHit = 0 else output.ShockChanceOnHit = m_min(100, skillModList:Sum("BASE", cfg, "EnemyShockChance") + enemyDB:Sum("BASE", nil, "SelfShockChance")) end if skillModList:Flag(cfg, "CannotFreeze") then output.FreezeChanceOnHit = 0 else output.FreezeChanceOnHit = m_min(100, skillModList:Sum("BASE", cfg, "EnemyFreezeChance") + enemyDB:Sum("BASE", nil, "SelfFreezeChance")) if skillModList:Flag(cfg, "CritsDontAlwaysFreeze") then output.FreezeChanceOnCrit = output.FreezeChanceOnHit end end if skillModList:Flag(cfg, "CannotKnockback") then output.KnockbackChanceOnHit = 0 else output.KnockbackChanceOnHit = skillModList:Sum("BASE", cfg, "EnemyKnockbackChance") end if skillFlags.attack and skillFlags.projectile and skillModList:Flag(cfg, "ArrowsThatPierceCauseBleeding") then output.BleedChanceOnHit = 100 - (1 - output.BleedChanceOnHit / 100) * (1 - globalOutput.PierceChance / 100) * 100 output.BleedChanceOnCrit = 100 - (1 - output.BleedChanceOnCrit / 100) * (1 - globalOutput.PierceChance / 100) * 100 end if env.mode_effective then local bleedMult = (1 - enemyDB:Sum("BASE", nil, "AvoidBleed") / 100) output.BleedChanceOnHit = output.BleedChanceOnHit * bleedMult output.BleedChanceOnCrit = output.BleedChanceOnCrit * bleedMult local poisonMult = (1 - enemyDB:Sum("BASE", nil, "AvoidPoison") / 100) output.PoisonChanceOnHit = output.PoisonChanceOnHit * poisonMult output.PoisonChanceOnCrit = output.PoisonChanceOnCrit * poisonMult output.ChaosPoisonChance = output.ChaosPoisonChance * poisonMult local igniteMult = (1 - enemyDB:Sum("BASE", nil, "AvoidIgnite") / 100) output.IgniteChanceOnHit = output.IgniteChanceOnHit * igniteMult output.IgniteChanceOnCrit = output.IgniteChanceOnCrit * igniteMult local shockMult = (1 - enemyDB:Sum("BASE", nil, "AvoidShock") / 100) output.ShockChanceOnHit = output.ShockChanceOnHit * shockMult output.ShockChanceOnCrit = output.ShockChanceOnCrit * shockMult local freezeMult = (1 - enemyDB:Sum("BASE", nil, "AvoidFreeze") / 100) output.FreezeChanceOnHit = output.FreezeChanceOnHit * freezeMult output.FreezeChanceOnCrit = output.FreezeChanceOnCrit * freezeMult end local function calcSecondaryEffectBase(type, sourceHitDmg, sourceCritDmg) -- Calculate the inflict chance and base damage of a secondary effect (bleed/poison/ignite/shock/freeze) local chanceOnHit, chanceOnCrit = output[type.."ChanceOnHit"], output[type.."ChanceOnCrit"] local chanceFromHit = chanceOnHit * (1 - output.CritChance / 100) local chanceFromCrit = chanceOnCrit * output.CritChance / 100 local chance = chanceFromHit + chanceFromCrit output[type.."Chance"] = chance local baseFromHit = sourceHitDmg * chanceFromHit / (chanceFromHit + chanceFromCrit) local baseFromCrit = sourceCritDmg * chanceFromCrit / (chanceFromHit + chanceFromCrit) local baseVal = baseFromHit + baseFromCrit if breakdown and chance ~= 0 then local breakdownChance = breakdown[type.."Chance"] or { } breakdown[type.."Chance"] = breakdownChance if breakdownChance[1] then t_insert(breakdownChance, "") end if isAttack then t_insert(breakdownChance, pass.label..":") end t_insert(breakdownChance, s_format("Chance on Non-crit: %d%%", chanceOnHit)) t_insert(breakdownChance, s_format("Chance on Crit: %d%%", chanceOnCrit)) if chanceOnHit ~= chanceOnCrit then t_insert(breakdownChance, "Combined chance:") t_insert(breakdownChance, s_format("%d x (1 - %.4f) ^8(chance from non-crits)", chanceOnHit, output.CritChance/100)) t_insert(breakdownChance, s_format("+ %d x %.4f ^8(chance from crits)", chanceOnCrit, output.CritChance/100)) t_insert(breakdownChance, s_format("= %.2f", chance)) end end if breakdown and baseVal > 0 then local breakdownDPS = breakdown[type.."DPS"] or { } breakdown[type.."DPS"] = breakdownDPS if breakdownDPS[1] then t_insert(breakdownDPS, "") end if isAttack then t_insert(breakdownDPS, pass.label..":") end if sourceHitDmg == sourceCritDmg then t_insert(breakdownDPS, "Base damage:") t_insert(breakdownDPS, s_format("%.1f ^8(source damage)",sourceHitDmg)) else if baseFromHit > 0 then t_insert(breakdownDPS, "Base from Non-crits:") t_insert(breakdownDPS, s_format("%.1f ^8(source damage from non-crits)", sourceHitDmg)) t_insert(breakdownDPS, s_format("x %.3f ^8(portion of instances created by non-crits)", chanceFromHit / (chanceFromHit + chanceFromCrit))) t_insert(breakdownDPS, s_format("= %.1f", baseFromHit)) end if baseFromCrit > 0 then t_insert(breakdownDPS, "Base from Crits:") t_insert(breakdownDPS, s_format("%.1f ^8(source damage from crits)", sourceCritDmg)) t_insert(breakdownDPS, s_format("x %.3f ^8(portion of instances created by crits)", chanceFromCrit / (chanceFromHit + chanceFromCrit))) t_insert(breakdownDPS, s_format("= %.1f", baseFromCrit)) end if baseFromHit > 0 and baseFromCrit > 0 then t_insert(breakdownDPS, "Total base damage:") t_insert(breakdownDPS, s_format("%.1f + %.1f", baseFromHit, baseFromCrit)) t_insert(breakdownDPS, s_format("= %.1f", baseVal)) end end end return baseVal end -- Calculate bleeding chance and damage if canDeal.Physical and (output.BleedChanceOnHit + output.BleedChanceOnCrit) > 0 then local sourceHitDmg = output.PhysicalHitAverage local sourceCritDmg = output.PhysicalCritAverage local basePercent = skillData.bleedBasePercent or 10 local baseVal = calcSecondaryEffectBase("Bleed", sourceHitDmg, sourceCritDmg) * basePercent / 100 if baseVal > 0 then skillFlags.bleed = true skillFlags.duration = true if not activeSkill.bleedCfg then activeSkill.bleedCfg = { skillName = skillCfg.skillName, skillTypes = skillCfg.skillTypes, slotName = skillCfg.slotName, flags = bor(band(skillCfg.flags, ModFlag.SourceMask), ModFlag.Dot, skillData.dotIsSpell and ModFlag.Spell or 0), keywordFlags = bor(skillCfg.keywordFlags, KeywordFlag.Bleed) } end local dotCfg = activeSkill.bleedCfg local effMult = 1 if env.mode_effective then local resist = enemyDB:Sum("BASE", nil, "PhysicalDamageReduction") local taken = enemyDB:Sum("INC", dotCfg, "DamageTaken", "DamageTakenOverTime", "PhysicalDamageTaken", "PhysicalDamageTakenOverTime") effMult = (1 - resist / 100) * (1 + taken / 100) globalOutput["BleedEffMult"] = effMult if breakdown and effMult ~= 1 then globalBreakdown.BleedEffMult = breakdown.effMult("Physical", resist, 0, taken, effMult) end end local inc = skillModList:Sum("INC", dotCfg, "Damage", "PhysicalDamage") local more = round(skillModList:More(dotCfg, "Damage", "PhysicalDamage"), 2) output.BleedDPS = baseVal * (1 + inc/100) * more * effMult local durationMod = calcLib.mod(skillModList, dotCfg, "Duration") * calcLib.mod(enemyDB, nil, "SelfBleedDuration") globalOutput.BleedDuration = 5 * durationMod * debuffDurationMult if breakdown then t_insert(breakdown.BleedDPS, s_format("x %.2f ^8(bleed deals %d%% per second)", basePercent/100, basePercent)) t_insert(breakdown.BleedDPS, s_format("= %.1f", baseVal)) t_insert(breakdown.BleedDPS, "Bleed DPS:") breakdown.dot(breakdown.BleedDPS, baseVal, inc, more, nil, nil, effMult, output.BleedDPS) if globalOutput.BleedDuration ~= 5 then globalBreakdown.BleedDuration = { "5.00s ^8(base duration)" } if durationMod ~= 1 then t_insert(globalBreakdown.BleedDuration, s_format("x %.2f ^8(duration modifier)", durationMod)) end if debuffDurationMult ~= 1 then t_insert(globalBreakdown.BleedDuration, s_format("/ %.2f ^8(debuff expires slower/faster)", 1 / debuffDurationMult)) end t_insert(globalBreakdown.BleedDuration, s_format("= %.2fs", globalOutput.BleedDuration)) end end end end -- Calculate poison chance and damage if canDeal.Chaos and (output.PoisonChanceOnHit + output.PoisonChanceOnCrit + output.ChaosPoisonChance) > 0 then local sourceHitDmg = output.ChaosHitAverage if output.ChaosPoisonChance > 0 and sourceHitDmg > 0 then -- Additional chance for chaos; adjust Physical damage and inflict chance local chaosChance = m_min(100, output.PoisonChanceOnHit + output.ChaosPoisonChance) sourceHitDmg = sourceHitDmg + output.PhysicalHitAverage * output.PoisonChanceOnHit / chaosChance output.PoisonChanceOnHit = chaosChance else sourceHitDmg = sourceHitDmg + output.PhysicalHitAverage end local sourceCritDmg = output.ChaosCritAverage if output.ChaosPoisonChance > 0 and sourceCritDmg > 0 then -- Additional chance for chaos; adjust Physical damage and inflict chance local chaosChance = m_min(100, output.PoisonChanceOnCrit + output.ChaosPoisonChance) sourceCritDmg = sourceCritDmg + output.PhysicalCritAverage * output.PoisonChanceOnCrit / chaosChance output.PoisonChanceOnCrit = chaosChance else sourceCritDmg = sourceCritDmg + output.PhysicalCritAverage end local baseVal = calcSecondaryEffectBase("Poison", sourceHitDmg, sourceCritDmg * skillModList:More(cfg, "PoisonDamageOnCrit")) * 0.08 if baseVal > 0 then skillFlags.poison = true skillFlags.duration = true if not activeSkill.poisonCfg then activeSkill.poisonCfg = { skillName = skillCfg.skillName, skillTypes = skillCfg.skillTypes, slotName = skillCfg.slotName, flags = bor(band(skillCfg.flags, ModFlag.SourceMask), ModFlag.Dot, skillData.dotIsSpell and ModFlag.Spell or 0), keywordFlags = bor(skillCfg.keywordFlags, KeywordFlag.Poison) } end local dotCfg = activeSkill.poisonCfg local effMult = 1 if env.mode_effective then local resist = m_min(enemyDB:Sum("BASE", nil, "ChaosResist"), 75) local taken = enemyDB:Sum("INC", nil, "DamageTaken", "DamageTakenOverTime", "ChaosDamageTaken", "ChaosDamageTakenOverTime") effMult = (1 - resist / 100) * (1 + taken / 100) globalOutput["PoisonEffMult"] = effMult if breakdown and effMult ~= 1 then globalBreakdown.PoisonEffMult = breakdown.effMult("Chaos", resist, 0, taken, effMult) end end local inc = skillModList:Sum("INC", dotCfg, "Damage", "ChaosDamage") local more = round(skillModList:More(dotCfg, "Damage", "ChaosDamage"), 2) output.PoisonDPS = baseVal * (1 + inc/100) * more * effMult local durationBase if skillData.poisonDurationIsSkillDuration then durationBase = skillData.duration else durationBase = 2 end local durationMod = calcLib.mod(skillModList, dotCfg, "Duration") * calcLib.mod(enemyDB, nil, "SelfPoisonDuration") globalOutput.PoisonDuration = durationBase * durationMod * debuffDurationMult output.PoisonDamage = output.PoisonDPS * globalOutput.PoisonDuration if skillData.showAverage then output.TotalPoisonAverageDamage = output.HitChance / 100 * output.PoisonChance / 100 * output.PoisonDamage else output.TotalPoisonStacks = output.HitChance / 100 * output.PoisonChance / 100 * globalOutput.PoisonDuration * (globalOutput.HitSpeed or globalOutput.Speed) * (skillData.dpsMultiplier or 1) output.TotalPoisonDPS = output.PoisonDPS * output.TotalPoisonStacks end if breakdown then t_insert(breakdown.PoisonDPS, "x 0.08 ^8(poison deals 8% per second)") t_insert(breakdown.PoisonDPS, s_format("= %.1f", baseVal, 1)) t_insert(breakdown.PoisonDPS, "Poison DPS:") breakdown.dot(breakdown.PoisonDPS, baseVal, inc, more, nil, nil, effMult, output.PoisonDPS) if globalOutput.PoisonDuration ~= 2 then globalBreakdown.PoisonDuration = { s_format("%.2fs ^8(base duration)", durationBase) } if durationMod ~= 1 then t_insert(globalBreakdown.PoisonDuration, s_format("x %.2f ^8(duration modifier)", durationMod)) end if debuffDurationMult ~= 1 then t_insert(globalBreakdown.PoisonDuration, s_format("/ %.2f ^8(debuff expires slower/faster)", 1 / debuffDurationMult)) end t_insert(globalBreakdown.PoisonDuration, s_format("= %.2fs", globalOutput.PoisonDuration)) end breakdown.PoisonDamage = { } if isAttack then t_insert(breakdown.PoisonDamage, pass.label..":") end t_insert(breakdown.PoisonDamage, s_format("%.1f ^8(damage per second)", output.PoisonDPS)) t_insert(breakdown.PoisonDamage, s_format("x %.2fs ^8(poison duration)", globalOutput.PoisonDuration)) t_insert(breakdown.PoisonDamage, s_format("= %.1f ^8damage per poison stack", output.PoisonDamage)) if not skillData.showAverage then breakdown.TotalPoisonStacks = { } if isAttack then t_insert(breakdown.TotalPoisonStacks, pass.label..":") end breakdown.multiChain(breakdown.TotalPoisonStacks, { base = s_format("%.2fs ^8(poison duration)", globalOutput.PoisonDuration), { "%.2f ^8(poison chance)", output.PoisonChance / 100 }, { "%.2f ^8(hit chance)", output.HitChance / 100 }, { "%.2f ^8(hits per second)", globalOutput.HitSpeed or globalOutput.Speed }, { "%g ^8(dps multiplier for this skill)", skillData.dpsMultiplier or 1 }, total = s_format("= %.1f", output.TotalPoisonStacks), }) end end end end -- Calculate ignite chance and damage if canDeal.Fire and (output.IgniteChanceOnHit + output.IgniteChanceOnCrit) > 0 then local sourceHitDmg = 0 local sourceCritDmg = 0 if canDeal.Physical and skillModList:Flag(cfg, "PhysicalCanIgnite") then sourceHitDmg = sourceHitDmg + output.PhysicalHitAverage sourceCritDmg = sourceCritDmg + output.PhysicalCritAverage end if canDeal.Lightning and skillModList:Flag(cfg, "LightningCanIgnite") then sourceHitDmg = sourceHitDmg + output.LightningHitAverage sourceCritDmg = sourceCritDmg + output.LightningCritAverage end if canDeal.Cold and skillModList:Flag(cfg, "ColdCanIgnite") then sourceHitDmg = sourceHitDmg + output.ColdHitAverage sourceCritDmg = sourceCritDmg + output.ColdCritAverage end if canDeal.Fire and not skillModList:Flag(cfg, "FireCannotIgnite") then sourceHitDmg = sourceHitDmg + output.FireHitAverage sourceCritDmg = sourceCritDmg + output.FireCritAverage end if canDeal.Chaos and skillModList:Flag(cfg, "ChaosCanIgnite") then sourceHitDmg = sourceHitDmg + output.ChaosHitAverage sourceCritDmg = sourceCritDmg + output.ChaosCritAverage end local igniteMode = env.configInput.igniteMode or "AVERAGE" if igniteMode == "CRIT" then output.IgniteChanceOnHit = 0 end if globalBreakdown then globalBreakdown.IgniteDPS = { s_format("Ignite mode: %s ^8(can be changed in the Configuration tab)", igniteMode == "CRIT" and "Crit Damage" or "Average Damage") } end local baseVal = calcSecondaryEffectBase("Ignite", sourceHitDmg, sourceCritDmg) * 0.2 if baseVal > 0 then skillFlags.ignite = true if not activeSkill.igniteCfg then activeSkill.igniteCfg = { skillName = skillCfg.skillName, skillTypes = skillCfg.skillTypes, slotName = skillCfg.slotName, flags = bor(band(skillCfg.flags, ModFlag.SourceMask), ModFlag.Dot, skillData.dotIsSpell and ModFlag.Spell or 0), keywordFlags = skillCfg.keywordFlags, } end local dotCfg = activeSkill.igniteCfg local effMult = 1 if env.mode_effective then local resist = m_min(enemyDB:Sum("BASE", nil, "FireResist", "ElementalResist"), 75) local taken = enemyDB:Sum("INC", dotCfg, "DamageTaken", "DamageTakenOverTime", "FireDamageTaken", "FireDamageTakenOverTime", "ElementalDamageTaken") effMult = (1 - resist / 100) * (1 + taken / 100) globalOutput["IgniteEffMult"] = effMult if breakdown and effMult ~= 1 then globalBreakdown.IgniteEffMult = breakdown.effMult("Fire", resist, 0, taken, effMult) end end local inc = skillModList:Sum("INC", dotCfg, "Damage", "FireDamage", "ElementalDamage") local more = round(skillModList:More(dotCfg, "Damage", "FireDamage", "ElementalDamage"), 2) local burnRateMod = calcLib.mod(skillModList, cfg, "IgniteBurnFaster") / calcLib.mod(skillModList, cfg, "IgniteBurnSlower") output.IgniteDPS = baseVal * (1 + inc/100) * more * burnRateMod * effMult local incDur = skillModList:Sum("INC", dotCfg, "EnemyIgniteDuration") + enemyDB:Sum("INC", nil, "SelfIgniteDuration") local moreDur = enemyDB:More(nil, "SelfIgniteDuration") globalOutput.IgniteDuration = 4 * (1 + incDur / 100) * moreDur / burnRateMod * debuffDurationMult if skillFlags.igniteCanStack then output.IgniteDamage = output.IgniteDPS * globalOutput.IgniteDuration if skillData.showAverage then output.TotalIgniteAverageDamage = output.HitChance / 100 * output.IgniteChance / 100 * output.IgniteDamage else output.TotalIgniteStacks = output.HitChance / 100 * output.IgniteChance / 100 * globalOutput.IgniteDuration * (globalOutput.HitSpeed or globalOutput.Speed) * (skillData.dpsMultiplier or 1) output.TotalIgniteDPS = output.IgniteDPS * output.TotalIgniteStacks end end if breakdown then t_insert(breakdown.IgniteDPS, "x 0.2 ^8(ignite deals 20% per second)") t_insert(breakdown.IgniteDPS, s_format("= %.1f", baseVal, 1)) t_insert(breakdown.IgniteDPS, "Ignite DPS:") breakdown.dot(breakdown.IgniteDPS, baseVal, inc, more, nil, burnRateMod, effMult, output.IgniteDPS) if skillFlags.igniteCanStack then breakdown.IgniteDamage = { } if isAttack then t_insert(breakdown.IgniteDamage, pass.label..":") end t_insert(breakdown.IgniteDamage, s_format("%.1f ^8(damage per second)", output.IgniteDPS)) t_insert(breakdown.IgniteDamage, s_format("x %.2fs ^8(ignite duration)", globalOutput.IgniteDuration)) t_insert(breakdown.IgniteDamage, s_format("= %.1f ^8damage per ignite stack", output.IgniteDamage)) if not skillData.showAverage then breakdown.TotalIgniteStacks = { } if isAttack then t_insert(breakdown.TotalIgniteStacks, pass.label..":") end breakdown.multiChain(breakdown.TotalIgniteStacks, { base = s_format("%.2fs ^8(ignite duration)", globalOutput.IgniteDuration), { "%.2f ^8(ignite chance)", output.IgniteChance / 100 }, { "%.2f ^8(hit chance)", output.HitChance / 100 }, { "%.2f ^8(hits per second)", globalOutput.HitSpeed or globalOutput.Speed }, { "%g ^8(dps multiplier for this skill)", skillData.dpsMultiplier or 1 }, total = s_format("= %.1f", output.TotalIgniteStacks), }) end end if globalOutput.IgniteDuration ~= 4 then globalBreakdown.IgniteDuration = { s_format("4.00s ^8(base duration)", durationBase) } if incDur ~= 0 then t_insert(globalBreakdown.IgniteDuration, s_format("x %.2f ^8(increased/reduced duration)", 1 + incDur/100)) end if moreDur ~= 1 then t_insert(globalBreakdown.IgniteDuration, s_format("x %.2f ^8(more/less duration)", moreDur)) end if burnRateMod ~= 1 then t_insert(globalBreakdown.IgniteDuration, s_format("/ %.2f ^8(rate modifier)", burnRateMod)) end if debuffDurationMult ~= 1 then t_insert(globalBreakdown.IgniteDuration, s_format("/ %.2f ^8(debuff expires slower/faster)", 1 / debuffDurationMult)) end t_insert(globalBreakdown.IgniteDuration, s_format("= %.2fs", globalOutput.IgniteDuration)) end end end end -- Calculate shock and freeze chance + duration modifier if (output.ShockChanceOnHit + output.ShockChanceOnCrit) > 0 then local sourceHitDmg = 0 local sourceCritDmg = 0 if canDeal.Physical and skillModList:Flag(cfg, "PhysicalCanShock") then sourceHitDmg = sourceHitDmg + output.PhysicalHitAverage sourceCritDmg = sourceCritDmg + output.PhysicalCritAverage end if canDeal.Lightning and not skillModList:Flag(cfg, "LightningCannotShock") then sourceHitDmg = sourceHitDmg + output.LightningHitAverage sourceCritDmg = sourceCritDmg + output.LightningCritAverage end if canDeal.Cold and skillModList:Flag(cfg, "ColdCanShock") then sourceHitDmg = sourceHitDmg + output.ColdHitAverage sourceCritDmg = sourceCritDmg + output.ColdCritAverage end if canDeal.Fire and skillModList:Flag(cfg, "FireCanShock") then sourceHitDmg = sourceHitDmg + output.FireHitAverage sourceCritDmg = sourceCritDmg + output.FireCritAverage end if canDeal.Chaos and skillModList:Flag(cfg, "ChaosCanShock") then sourceHitDmg = sourceHitDmg + output.ChaosHitAverage sourceCritDmg = sourceCritDmg + output.ChaosCritAverage end local baseVal = calcSecondaryEffectBase("Shock", sourceHitDmg, sourceCritDmg) if baseVal > 0 then skillFlags.shock = true output.ShockDurationMod = 1 + skillModList:Sum("INC", cfg, "EnemyShockDuration") / 100 + enemyDB:Sum("INC", nil, "SelfShockDuration") / 100 if breakdown then t_insert(breakdown.ShockDPS, s_format("For shock to apply, target must have no more than %d life.", baseVal * 20 * output.ShockDurationMod)) end end end if (output.FreezeChanceOnHit + output.FreezeChanceOnCrit) > 0 then local sourceHitDmg = 0 local sourceCritDmg = 0 if canDeal.Cold and not skillModList:Flag(cfg, "ColdCannotFreeze") then sourceHitDmg = sourceHitDmg + output.ColdHitAverage sourceCritDmg = sourceCritDmg + output.ColdCritAverage end if canDeal.Lightning and skillModList:Flag(cfg, "LightningCanFreeze") then sourceHitDmg = sourceHitDmg + output.LightningHitAverage sourceCritDmg = sourceCritDmg + output.LightningCritAverage end local baseVal = calcSecondaryEffectBase("Freeze", sourceHitDmg, sourceCritDmg) if baseVal > 0 then skillFlags.freeze = true output.FreezeDurationMod = 1 + skillModList:Sum("INC", cfg, "EnemyFreezeDuration") / 100 + enemyDB:Sum("INC", nil, "SelfFreezeDuration") / 100 if breakdown then t_insert(breakdown.FreezeDPS, s_format("For freeze to apply, target must have no more than %d life.", baseVal * 20 * output.FreezeDurationMod)) end end end -- Calculate knockback chance/distance output.KnockbackChance = m_min(100, output.KnockbackChanceOnHit * (1 - output.CritChance / 100) + output.KnockbackChanceOnCrit * output.CritChance / 100 + enemyDB:Sum("BASE", nil, "SelfKnockbackChance")) if output.KnockbackChance > 0 then output.KnockbackDistance = round(4 * calcLib.mod(skillModList, cfg, "EnemyKnockbackDistance")) if breakdown then breakdown.KnockbackDistance = { radius = output.KnockbackDistance, } end end -- Calculate enemy stun modifiers local enemyStunThresholdRed = -skillModList:Sum("INC", cfg, "EnemyStunThreshold") if enemyStunThresholdRed > 75 then output.EnemyStunThresholdMod = 1 - (75 + (enemyStunThresholdRed - 75) * 25 / (enemyStunThresholdRed - 50)) / 100 else output.EnemyStunThresholdMod = 1 - enemyStunThresholdRed / 100 end local incDur = skillModList:Sum("INC", cfg, "EnemyStunDuration") local incRecov = enemyDB:Sum("INC", nil, "StunRecovery") output.EnemyStunDuration = 0.35 * (1 + incDur / 100) / (1 + incRecov / 100) if breakdown then if output.EnemyStunDuration ~= 0.35 then breakdown.EnemyStunDuration = { "0.35s ^8(base duration)" } if incDur ~= 0 then t_insert(breakdown.EnemyStunDuration, s_format("x %.2f ^8(increased/reduced stun duration)", 1 + incDur/100)) end if incRecov ~= 0 then t_insert(breakdown.EnemyStunDuration, s_format("/ %.2f ^8(increased/reduced enemy stun recovery)", 1 + incRecov/100)) end t_insert(breakdown.EnemyStunDuration, s_format("= %.2fs", output.EnemyStunDuration)) end end end -- Combine secondary effect stats if isAttack then combineStat("BleedChance", "AVERAGE") combineStat("BleedDPS", "CHANCE", "BleedChance") combineStat("PoisonChance", "AVERAGE") combineStat("PoisonDPS", "CHANCE", "PoisonChance") combineStat("PoisonDamage", "CHANCE", "PoisonChance") if skillData.showAverage then combineStat("TotalPoisonAverageDamage", "DPS") else combineStat("TotalPoisonStacks", "DPS") combineStat("TotalPoisonDPS", "DPS") end combineStat("IgniteChance", "AVERAGE") combineStat("IgniteDPS", "CHANCE", "IgniteChance") if skillFlags.igniteCanStack then combineStat("IgniteDamage", "CHANCE", "IgniteChance") if skillData.showAverage then combineStat("TotalIgniteAverageDamage", "DPS") else combineStat("TotalIgniteStacks", "DPS") combineStat("TotalIgniteDPS", "DPS") end end combineStat("ShockChance", "AVERAGE") combineStat("ShockDurationMod", "AVERAGE") combineStat("FreezeChance", "AVERAGE") combineStat("FreezeDurationMod", "AVERAGE") end if skillFlags.hit and skillData.decay then -- Calculate DPS for Essence of Delirium's Decay effect skillFlags.decay = true activeSkill.decayCfg = { slotName = skillCfg.slotName, skillTypes = skillCfg.skillTypes, flags = bor(band(skillCfg.flags, ModFlag.SourceMask), ModFlag.Dot, skillData.dotIsSpell and ModFlag.Spell or 0), keywordFlags = skillCfg.keywordFlags, } local dotCfg = activeSkill.decayCfg local effMult = 1 if env.mode_effective then local resist = m_min(enemyDB:Sum("BASE", nil, "ChaosResist"), 75) local taken = enemyDB:Sum("INC", nil, "DamageTaken", "DamageTakenOverTime", "ChaosDamageTaken", "ChaosDamageTakenOverTime") effMult = (1 - resist / 100) * (1 + taken / 100) output["DecayEffMult"] = effMult if breakdown and effMult ~= 1 then breakdown.DecayEffMult = breakdown.effMult("Chaos", resist, 0, taken, effMult) end end local inc = skillModList:Sum("INC", dotCfg, "Damage", "ChaosDamage") local more = round(skillModList:More(dotCfg, "Damage", "ChaosDamage"), 2) output.DecayDPS = skillData.decay * (1 + inc/100) * more * effMult local durationMod = calcLib.mod(skillModList, dotCfg, "Duration") output.DecayDuration = 10 * durationMod * debuffDurationMult if breakdown then breakdown.DecayDPS = { } t_insert(breakdown.DecayDPS, "Decay DPS:") breakdown.dot(breakdown.DecayDPS, skillData.decay, inc, more, nil, nil, effMult, output.DecayDPS) if output.DecayDuration ~= 2 then breakdown.DecayDuration = { s_format("%.2fs ^8(base duration)", 10) } if durationMod ~= 1 then t_insert(breakdown.DecayDuration, s_format("x %.2f ^8(duration modifier)", durationMod)) end if debuffDurationMult ~= 1 then t_insert(breakdown.DecayDuration, s_format("/ %.2f ^8(debuff expires slower/faster)", 1 / debuffDurationMult)) end t_insert(breakdown.DecayDuration, s_format("= %.2fs", output.DecayDuration)) end end end -- Calculate combined DPS estimate, including DoTs local baseDPS = output[(skillData.showAverage and "AverageDamage") or "TotalDPS"] + output.TotalDot output.CombinedDPS = baseDPS if skillData.showAverage then output.CombinedDPS = output.CombinedDPS + (output.TotalPoisonAverageDamage or 0) output.WithPoisonAverageDamage = baseDPS + (output.TotalPoisonAverageDamage or 0) else output.CombinedDPS = output.CombinedDPS + (output.TotalPoisonDPS or 0) output.WithPoisonDPS = baseDPS + (output.TotalPoisonDPS or 0) end if skillFlags.ignite then if skillFlags.igniteCanStack then if skillData.showAverage then output.CombinedDPS = output.CombinedDPS + output.TotalIgniteAverageDamage output.WithIgniteAverageDamage = baseDPS + output.TotalIgniteAverageDamage else output.CombinedDPS = output.CombinedDPS + output.TotalIgniteDPS output.WithIgniteDPS = baseDPS + output.TotalIgniteDPS end else output.CombinedDPS = output.CombinedDPS + output.IgniteDPS end end if skillFlags.bleed then output.CombinedDPS = output.CombinedDPS + output.BleedDPS end if skillFlags.decay then output.CombinedDPS = output.CombinedDPS + output.DecayDPS end end