-- Path of Building -- -- Module: Calcs -- Performs all the offense and defense calculations. -- Here be dragons! -- This file is 3100 lines long, over half of which is in one function... -- local pairs = pairs local ipairs = ipairs local t_insert = table.insert local t_remove = table.remove local m_abs = math.abs local m_ceil = math.ceil local m_floor = math.floor local m_min = math.min local m_max = math.max local s_format = string.format local band = bit.band local bor = bit.bor local bnot = bit.bnot -- List of all damage types, ordered according to the conversion sequence local dmgTypeList = {"Physical", "Lightning", "Cold", "Fire", "Chaos"} local resistTypeList = { "Fire", "Cold", "Lightning", "Chaos" } local isElemental = { Fire = true, Cold = true, Lightning = true } -- Calculate and combine INC/MORE modifiers for the given modifier names local function calcMod(modDB, cfg, ...) return (1 + (modDB:Sum("INC", cfg, ...)) / 100) * modDB:Sum("MORE", cfg, ...) end -- Calculate value, optionally adding additional base local function calcVal(modDB, name, cfg, base) local baseVal = modDB:Sum("BASE", cfg, name) + (base or 0) if baseVal ~= 0 then return baseVal * calcMod(modDB, cfg, name) else return 0 end end -- Calculate hit chance local function calcHitChance(evasion, accuracy) local rawChance = accuracy / (accuracy + (evasion / 4) ^ 0.8) * 100 return m_max(m_min(m_floor(rawChance + 0.5), 95), 5) end -- Merge gem modifiers with given mod list local function mergeGemMods(modList, gem) modList:AddList(gem.data.baseMods) if gem.quality > 0 then for i = 1, #gem.data.qualityMods do local scaledMod = copyTable(gem.data.qualityMods[i]) scaledMod.value = m_floor(scaledMod.value * gem.quality) modList:AddMod(scaledMod) end end gem.level = m_max(gem.level, 1) if not gem.data.levels[gem.level] then gem.level = m_min(gem.level, #gem.data.levels) end local levelData = gem.data.levels[gem.level] for col, mod in pairs(gem.data.levelMods) do if levelData[col] then local newMod = copyTable(mod) if type(newMod.value) == "table" then newMod.value.value = levelData[col] else newMod.value = levelData[col] end modList:AddMod(newMod) end end end -- Check if given support gem can support the given active skill -- Global function, as GemSelectControl needs to use it too function gemCanSupport(gem, activeSkill) if gem.data.unsupported then return false end for _, skillType in pairs(gem.data.excludeSkillTypes) do if activeSkill.skillTypes[skillType] then return false end end if not gem.data.requireSkillTypes[1] then return true end for _, skillType in pairs(gem.data.requireSkillTypes) do if activeSkill.skillTypes[skillType] then return true end end return false end -- Check if given gem is of the given type ("all", "strength", "melee", etc) -- Global function, as ModDBClass and ModListClass need to use it too function gemIsType(gem, type) return type == "all" or (type == "elemental" and (gem.data.fire or gem.data.cold or gem.data.lightning)) or gem.data[type] end -- Create an active skill using the given active gem and list of support gems -- It will determine the base flag set, and check which of the support gems can support this skill local function createActiveSkill(activeGem, supportList) local activeSkill = { } activeSkill.activeGem = { name = activeGem.name, data = activeGem.data, level = activeGem.level, quality = activeGem.quality, fromItem = activeGem.fromItem, srcGem = activeGem, } activeSkill.gemList = { activeSkill.activeGem } activeSkill.skillTypes = copyTable(activeGem.data.skillTypes) -- Initialise skill flag set ('attack', 'projectile', etc) local skillFlags = copyTable(activeGem.data.baseFlags) activeSkill.skillFlags = skillFlags skillFlags.hit = activeSkill.skillTypes[SkillType.Attack] or activeSkill.skillTypes[SkillType.Hit] for _, gem in ipairs(supportList) do if gemCanSupport(gem, activeSkill) then if gem.data.addFlags then -- Support gem adds flags to supported skills (eg. Remote Mine adds 'mine') for k in pairs(gem.data.addFlags) do skillFlags[k] = true end end for _, skillType in pairs(gem.data.addSkillTypes) do activeSkill.skillTypes[skillType] = true end end end -- Process support gems for _, gem in ipairs(supportList) do if gemCanSupport(gem, activeSkill) then t_insert(activeSkill.gemList, { name = gem.name, data = gem.data, level = gem.level, quality = gem.quality, fromItem = gem.fromItem, srcGem = gem, }) if gem.isSupporting then gem.isSupporting[activeGem.name] = true end end end return activeSkill end local function getWeaponFlags(weaponData, weaponTypes) local info = data.weaponTypeInfo[weaponData.type] if not info then return end if weaponTypes and not weaponTypes[weaponData.type] and (not weaponData.countsAsAll1H or not (weaponTypes["Claw"] or weaponTypes["Dagger"] or weaponTypes["One Handed Axe"] or weaponTypes["One Handed Mace"] or weaponTypes["One Handed Sword"])) then return end local flags = info.flag if weaponData.countsAsAll1H then flags = bor(ModFlag.Axe, ModFlag.Claw, ModFlag.Dagger, ModFlag.Mace, ModFlag.Sword) end if weaponData.type ~= "None" then flags = bor(flags, ModFlag.Weapon) if info.oneHand then flags = bor(flags, ModFlag.Weapon1H) else flags = bor(flags, ModFlag.Weapon2H) end if info.melee then flags = bor(flags, ModFlag.WeaponMelee) else flags = bor(flags, ModFlag.WeaponRanged) end end return flags, info end -- Build list of modifiers for given active skill local function buildActiveSkillModList(env, activeSkill) local skillTypes = activeSkill.skillTypes local skillFlags = activeSkill.skillFlags -- Handle multipart skills local activeGemParts = activeSkill.activeGem.data.parts if activeGemParts then if activeSkill == env.mainSkill then activeSkill.skillPart = m_min(#activeGemParts, env.skillPart or activeSkill.activeGem.srcGem.skillPart or 1) else activeSkill.skillPart = m_min(#activeGemParts, activeSkill.activeGem.srcGem.skillPart or 1) end local part = activeGemParts[activeSkill.skillPart] for k, v in pairs(part) do if v == true then skillFlags[k] = true elseif v == false then skillFlags[k] = nil end end activeSkill.skillPartName = part.name skillFlags.multiPart = #activeGemParts > 1 end if skillTypes[SkillType.Shield] and (not env.itemList["Weapon 2"] or env.itemList["Weapon 2"].type ~= "Shield") then -- Skill requires a shield to be equipped skillFlags.disable = true end if skillFlags.attack then -- Set weapon flags local weaponTypes = activeSkill.activeGem.data.weaponTypes local weapon1Flags, weapon1Info = getWeaponFlags(env.weaponData1, weaponTypes) if weapon1Flags then activeSkill.weapon1Flags = weapon1Flags skillFlags.weapon1Attack = true if weapon1Info.melee and skillFlags.melee then skillFlags.projectile = nil elseif not weapon1Info.melee and skillFlags.projectile then skillFlags.melee = nil end elseif skillTypes[SkillType.DualWield] or not skillTypes[SkillType.CanDualWield] or skillTypes[SkillType.MainHandOnly] then -- Skill requires a compatible main hand weapon skillFlags.disable = true end if skillTypes[SkillType.DualWield] or skillTypes[SkillType.CanDualWield] then if not skillTypes[SkillType.MainHandOnly] then local weapon2Flags = getWeaponFlags(env.weaponData2, weaponTypes) if weapon2Flags then activeSkill.weapon2Flags = weapon2Flags skillFlags.weapon2Attack = true elseif skillTypes[SkillType.DualWield] or not skillFlags.weapon1Attack then -- Skill requires a compatible off hand weapon skillFlags.disable = true end end elseif env.weaponData2.type then -- Skill cannot be used while dual wielding skillFlags.disable = true end skillFlags.bothWeaponAttack = skillFlags.weapon1Attack and skillFlags.weapon2Attack end -- Build skill mod flag set local skillModFlags = 0 if skillFlags.hit then skillModFlags = bor(skillModFlags, ModFlag.Hit) end if skillFlags.attack then skillModFlags = bor(skillModFlags, ModFlag.Attack) else skillModFlags = bor(skillModFlags, ModFlag.Cast) if skillFlags.spell then skillModFlags = bor(skillModFlags, ModFlag.Spell) end end if skillFlags.melee then skillModFlags = bor(skillModFlags, ModFlag.Melee) elseif skillFlags.projectile then skillModFlags = bor(skillModFlags, ModFlag.Projectile) end if skillFlags.area then skillModFlags = bor(skillModFlags, ModFlag.Area) end -- Build skill keyword flag set local skillKeywordFlags = 0 if skillFlags.aura then skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Aura) end if skillFlags.curse then skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Curse) end if skillFlags.warcry then skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Warcry) end if skillFlags.movement then skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Movement) end if skillFlags.vaal then skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Vaal) end if skillFlags.lightning then skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Lightning) end if skillFlags.cold then skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Cold) end if skillFlags.fire then skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Fire) end if skillFlags.chaos then skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Chaos) end if skillFlags.minion then skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Minion) elseif skillFlags.totem then skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Totem) elseif skillFlags.trap then skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Trap) elseif skillFlags.mine then skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Mine) end -- Get skill totem ID for totem skills -- This is used to calculate totem life if skillFlags.totem then activeSkill.skillTotemId = activeSkill.activeGem.data.skillTotemId if not activeSkill.skillTotemId then if activeSkill.activeGem.data.color == 2 then activeSkill.skillTotemId = 2 elseif activeSkill.activeGem.data.color == 3 then activeSkill.skillTotemId = 3 else activeSkill.skillTotemId = 1 end end end -- Build config structure for modifier searches activeSkill.skillCfg = { flags = bor(skillModFlags, activeSkill.weapon1Flags or activeSkill.weapon2Flags or 0), keywordFlags = skillKeywordFlags, skillName = activeSkill.activeGem.name:gsub("^Vaal ",""), -- This allows modifiers that target specific skills to also apply to their Vaal counterpart skillGem = activeSkill.activeGem, skillPart = activeSkill.skillPart, skillTypes = activeSkill.skillTypes, skillCond = { }, slotName = activeSkill.slotName, } if skillFlags.weapon1Attack then activeSkill.weapon1Cfg = copyTable(activeSkill.skillCfg, true) activeSkill.weapon1Cfg.skillCond = { ["MainHandAttack"] = true } activeSkill.weapon1Cfg.flags = bor(skillModFlags, activeSkill.weapon1Flags) end if skillFlags.weapon2Attack then activeSkill.weapon2Cfg = copyTable(activeSkill.skillCfg, true) activeSkill.weapon2Cfg.skillCond = { ["OffHandAttack"] = true } activeSkill.weapon2Cfg.flags = bor(skillModFlags, activeSkill.weapon2Flags) end -- Apply gem property modifiers from the item this skill is socketed into for _, value in ipairs(env.modDB:Sum("LIST", activeSkill.skillCfg, "GemProperty")) do for _, gem in pairs(activeSkill.gemList) do if not gem.fromItem and gemIsType(gem, value.keyword) then gem[value.key] = (gem[value.key] or 0) + value.value end end end -- Initialise skill modifier list local skillModList = common.New("ModList") activeSkill.skillModList = skillModList if skillFlags.disable then wipeTable(skillFlags) skillFlags.disable = true return end -- Add support gem modifiers to skill mod list for _, gem in pairs(activeSkill.gemList) do if gem.data.support then mergeGemMods(skillModList, gem) end end -- Apply gem/quality modifiers from support gems if not activeSkill.activeGem.fromItem then for _, value in ipairs(skillModList:Sum("LIST", activeSkill.skillCfg, "GemProperty")) do if value.keyword == "active_skill" then activeSkill.activeGem[value.key] = activeSkill.activeGem[value.key] + value.value end end end -- Add active gem modifiers mergeGemMods(skillModList, activeSkill.activeGem) -- Extract skill data activeSkill.skillData = { } for _, value in ipairs(skillModList:Sum("LIST", activeSkill.skillCfg, "Misc")) do if value.type == "SkillData" then activeSkill.skillData[value.key] = value.value end end -- Separate global effect modifiers (mods that can affect defensive stats or other skills) local i = 1 while skillModList[i] do local destList for _, tag in ipairs(skillModList[i].tagList) do if tag.type == "GlobalEffect" then if tag.effectType == "Buff" then destList = "buffModList" elseif tag.effectType == "Aura" then destList = "auraModList" elseif tag.effectType == "Debuff" then destList = "debuffModList" elseif tag.effectType == "Curse" then destList = "curseModList" end break end end if destList then if not activeSkill[destList] then activeSkill[destList] = { } end t_insert(activeSkill[destList], skillModList[i]) t_remove(skillModList, i) else i = i + 1 end end if activeSkill.buffModList or activeSkill.auraModList or activeSkill.debuffModList or activeSkill.curseModList then -- Add to auxillary skill list t_insert(env.auxSkillList, activeSkill) end end -- Build list of modifiers from the listed tree nodes local function buildNodeModList(env, nodeList, finishJewels) -- Initialise radius jewels for _, rad in pairs(env.radiusJewelList) do wipeTable(rad.data) end -- Add node modifers local modList = common.New("ModList") for _, node in pairs(nodeList) do -- Merge with output list if node.type == "keystone" then modList:AddMod(node.keystoneMod) else modList:AddList(node.modList) end -- Run radius jewels for _, rad in pairs(env.radiusJewelList) do if rad.nodes[node.id] then rad.func(node.modList, modList, rad.data) end end end if finishJewels then -- Finalise radius jewels for _, rad in pairs(env.radiusJewelList) do rad.func(nil, modList, rad.data) if env.mode == "MAIN" then if not rad.item.jewelRadiusData then rad.item.jewelRadiusData = { } end rad.item.jewelRadiusData[rad.nodeId] = rad.data end end end return modList end -- Calculate min/max damage of a hit for the given damage type local function calcHitDamage(env, source, cfg, breakdown, damageType, ...) local modDB = env.modDB local damageTypeMin = damageType.."Min" local damageTypeMax = damageType.."Max" -- Calculate base values local damageEffectiveness = source.damageEffectiveness or 1 local addedMin = modDB:Sum("BASE", cfg, damageTypeMin) local addedMax = modDB: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], env.mode_skillType == "ATTACK" 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 = env.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(env, 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.damageComponents, { 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 + modDB:Sum("INC", cfg, unpack(modNames)) / 100 local more = m_floor(modDB:Sum("MORE", cfg, unpack(modNames)) * 100 + 0.50000001) / 100 if breakdown then t_insert(breakdown.damageComponents, { 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 -- -- The following functions perform various steps in the calculations process. -- Depending on what is being done with the output, other code may run inbetween steps, however the steps must always be performed in order: -- 1. Initialise environment (initEnv) -- 2. Run calculations (performCalcs) -- -- Thus a basic calculation pass would look like this: -- -- local env = initEnv(build, mode) -- performCalcs(env) -- local tempTable1 = { } local tempTable2 = { } local tempTable3 = { } -- Initialise environment: -- 1. Initialises the modifier databases -- 2. Merges modifiers for all items -- 3. Builds a list of jewels with radius functions -- 4. Merges modifiers for all allocated passive nodes -- 5. Builds a list of active skills and their supports -- 6. Builds modifier lists for all active skills local function initEnv(build, mode, override) override = override or { } local env = { } env.build = build env.configInput = build.configTab.input env.calcsInput = build.calcsTab.input env.mode = mode env.spec = override.spec or build.spec env.classId = env.spec.curClassId -- Initialise modifier database with base values local modDB = common.New("ModDB") env.modDB = modDB local classStats = build.tree.characterData[env.classId] for _, stat in pairs({"Str","Dex","Int"}) do modDB:NewMod(stat, "BASE", classStats["base_"..stat:lower()], "Base") end modDB.multipliers["Level"] = m_max(1, m_min(100, build.characterLevel)) modDB:NewMod("Life", "BASE", 12, "Base", { type = "Multiplier", var = "Level", base = 38 }) modDB:NewMod("Mana", "BASE", 6, "Base", { type = "Multiplier", var = "Level", base = 34 }) modDB:NewMod("ManaRegen", "BASE", 0.0175, "Base", { type = "PerStat", stat = "Mana", div = 1 }) modDB:NewMod("Evasion", "BASE", 3, "Base", { type = "Multiplier", var = "Level", base = 53 }) modDB:NewMod("Accuracy", "BASE", 2, "Base", { type = "Multiplier", var = "Level", base = -2 }) modDB:NewMod("FireResistMax", "BASE", 75, "Base") modDB:NewMod("FireResist", "BASE", -60, "Base") modDB:NewMod("ColdResistMax", "BASE", 75, "Base") modDB:NewMod("ColdResist", "BASE", -60, "Base") modDB:NewMod("LightningResistMax", "BASE", 75, "Base") modDB:NewMod("LightningResist", "BASE", -60, "Base") modDB:NewMod("ChaosResistMax", "BASE", 75, "Base") modDB:NewMod("ChaosResist", "BASE", -60, "Base") modDB:NewMod("BlockChanceMax", "BASE", 75, "Base") modDB:NewMod("PowerChargesMax", "BASE", 3, "Base") modDB:NewMod("CritChance", "INC", 50, "Base", { type = "Multiplier", var = "PowerCharge" }) modDB:NewMod("FrenzyChargesMax", "BASE", 3, "Base") modDB:NewMod("Speed", "INC", 4, "Base", { type = "Multiplier", var = "FrenzyCharge" }) modDB:NewMod("Damage", "MORE", 4, "Base", { type = "Multiplier", var = "FrenzyCharge" }) modDB:NewMod("EnduranceChargesMax", "BASE", 3, "Base") modDB:NewMod("ElementalResist", "BASE", 4, "Base", { type = "Multiplier", var = "EnduranceCharge" }) modDB:NewMod("ActiveTrapLimit", "BASE", 3, "Base") modDB:NewMod("ActiveMineLimit", "BASE", 5, "Base") modDB:NewMod("ActiveTotemLimit", "BASE", 1, "Base") modDB:NewMod("ProjectileCount", "BASE", 1, "Base") modDB:NewMod("Speed", "MORE", 10, "Base", ModFlag.Attack, { type = "Condition", var = "DualWielding" }) modDB:NewMod("PhysicalDamage", "MORE", 20, "Base", ModFlag.Attack, { type = "Condition", var = "DualWielding" }) modDB:NewMod("BlockChance", "BASE", 15, "Base", { type = "Condition", var = "DualWielding" }) modDB:NewMod("LifeRegenPercent", "BASE", 4, "Base", { type = "Condition", var = "OnConsecratedGround" }) modDB:NewMod("Misc", "LIST", { type = "EnemyModifier", mod = modLib.createMod("DamageTaken", "INC", 50, "Shock") }, "Base", { type = "Condition", var = "EnemyShocked" }) modDB:NewMod("Misc", "LIST", { type = "EnemyModifier", mod = modLib.createMod("HitChance", "MORE", -50, "Blind") }, "Base", { type = "Condition", var = "EnemyBlinded" }) -- Add bandit mods if build.banditNormal == "Alira" then modDB:NewMod("Mana", "BASE", 60, "Bandit") elseif build.banditNormal == "Kraityn" then modDB:NewMod("ElementalResist", "BASE", 10, "Bandit") elseif build.banditNormal == "Oak" then modDB:NewMod("Life", "BASE", 40, "Bandit") else modDB:NewMod("ExtraPoints", "BASE", 1, "Bandit") end if build.banditCruel == "Alira" then modDB:NewMod("Speed", "INC", 5, "Bandit", ModFlag.Spell) elseif build.banditCruel == "Kraityn" then modDB:NewMod("Speed", "INC", 8, "Bandit", ModFlag.Attack) elseif build.banditCruel == "Oak" then modDB:NewMod("PhysicalDamage", "INC", 16, "Bandit") else modDB:NewMod("ExtraPoints", "BASE", 1, "Bandit") end if build.banditMerciless == "Alira" then modDB:NewMod("PowerChargesMax", "BASE", 1, "Bandit") elseif build.banditMerciless == "Kraityn" then modDB:NewMod("FrenzyChargesMax", "BASE", 1, "Bandit") elseif build.banditMerciless == "Oak" then modDB:NewMod("EnduranceChargesMax", "BASE", 1, "Bandit") else modDB:NewMod("ExtraPoints", "BASE", 1, "Bandit") end -- Initialise enemy modifier database local enemyDB = common.New("ModDB") env.enemyDB = enemyDB env.enemyLevel = m_max(1, m_min(100, env.configInput.enemyLevel and env.configInput.enemyLevel or m_min(env.build.characterLevel, 84))) enemyDB:NewMod("Accuracy", "BASE", data.monsterAccuracyTable[env.enemyLevel], "Base") enemyDB:NewMod("Evasion", "BASE", data.monsterEvasionTable[env.enemyLevel], "Base") -- Add mods from the config tab modDB:AddList(build.configTab.modList) enemyDB:AddList(build.configTab.enemyModList) -- Build list of passive nodes local nodes if override.addNodes or override.removeNodes then nodes = { } if override.addNodes then for node in pairs(override.addNodes) do nodes[node.id] = node end end for _, node in pairs(env.spec.allocNodes) do if not override.removeNodes or not override.removeNodes[node] then nodes[node.id] = node end end else nodes = env.spec.allocNodes end -- Build and merge item modifiers, and create list of radius jewels env.radiusJewelList = wipeTable(env.radiusJewelList) env.itemList = { } env.flasks = { } env.modDB.conditions["UsingAllCorruptedItems"] = true for slotName, slot in pairs(build.itemsTab.slots) do local item if slotName == override.repSlotName then item = override.repItem elseif slot.nodeId and override.spec then item = build.itemsTab.list[env.spec.jewels[slot.nodeId]] else item = build.itemsTab.list[slot.selItemId] end if slot.nodeId then -- Slot is a jewel socket, check if socket is allocated if not nodes[slot.nodeId] then item = nil elseif item and item.jewelRadiusIndex then -- Jewel has a radius, add it to the list local funcList = item.jewelData.funcList or { function(nodeMods, out, data) -- Default function just tallies all stats in radius if nodeMods then for _, stat in pairs({"Str","Dex","Int"}) do data[stat] = (data[stat] or 0) + nodeMods:Sum("BASE", nil, stat) end end end } for _, func in ipairs(funcList) do local node = build.spec.nodes[slot.nodeId] t_insert(env.radiusJewelList, { nodes = node.nodesInRadius[item.jewelRadiusIndex], func = func, item = item, nodeId = slot.nodeId, data = { } }) end end end if item and item.type == "Flask" then if slot.active then env.flasks[item] = true end item = nil end env.itemList[slotName] = item if item then -- Merge mods for this item local srcList = item.modList or item.slotModList[slot.slotNum] env.modDB:AddList(srcList) if item.type ~= "Jewel" and item.type ~= "Flask" then -- Update item counts local key if item.rarity == "UNIQUE" then key = "UniqueItem" elseif item.rarity == "RARE" then key = "RareItem" elseif item.rarity == "MAGIC" then key = "MagicItem" else key = "NormalItem" end env.modDB.multipliers[key] = (env.modDB.multipliers[key] or 0) + 1 if item.corrupted then env.modDB.multipliers.CorruptedItem = (env.modDB.multipliers.CorruptedItem or 0) + 1 else env.modDB.conditions["UsingAllCorruptedItems"] = false end end end end if override.toggleFlask then if env.flasks[override.toggleFlask] then env.flasks[override.toggleFlask] = nil else env.flasks[override.toggleFlask] = true end end if env.mode == "MAIN" then -- Process extra skills granted by items local markList = { } for _, mod in ipairs(env.modDB.mods["ExtraSkill"] or { }) do -- Extract the name of the slot containing the item this skill was granted by local slotName for _, tag in ipairs(mod.tagList) do if tag.type == "SocketedIn" then slotName = tag.slotName break end end -- Check if a matching group already exists local group for index, socketGroup in pairs(build.skillsTab.socketGroupList) do if socketGroup.source == mod.source and socketGroup.slot == slotName then if socketGroup.gemList[1] and socketGroup.gemList[1].nameSpec == mod.value.name then group = socketGroup markList[socketGroup] = true break end end end if not group then -- Create a new group for this skill group = { label = "", enabled = true, gemList = { }, source = mod.source, slot = slotName } t_insert(build.skillsTab.socketGroupList, group) markList[group] = true end -- Update the group group.sourceItem = build.itemsTab.list[tonumber(mod.source:match("Item:(%d+):"))] wipeTable(group.gemList) t_insert(group.gemList, { nameSpec = mod.value.name, level = mod.value.level, quality = 0, enabled = true, fromItem = true, }) if mod.value.noSupports then group.noSupports = true else for _, socketGroup in pairs(build.skillsTab.socketGroupList) do -- Look for other groups that are socketed in the item if socketGroup.slot == slotName and not socketGroup.source then -- Add all support gems to the skill's group for _, gem in ipairs(socketGroup.gemList) do if gem.data and gem.data.support then t_insert(group.gemList, gem) end end end end end build.skillsTab:ProcessSocketGroup(group) end -- Remove any socket groups that no longer have a matching item local i = 1 while build.skillsTab.socketGroupList[i] do local socketGroup = build.skillsTab.socketGroupList[i] if socketGroup.source and not markList[socketGroup] then t_remove(build.skillsTab.socketGroupList, i) if build.skillsTab.displayGroup == socketGroup then build.skillsTab.displayGroup = nil end else i = i + 1 end end end -- Get the weapon data tables for the equipped weapons env.weaponData1 = env.itemList["Weapon 1"] and env.itemList["Weapon 1"].weaponData and env.itemList["Weapon 1"].weaponData[1] or copyTable(data.unarmedWeaponData[env.classId]) if env.weaponData1.countsAsDualWielding then env.weaponData2 = env.itemList["Weapon 1"].weaponData[2] else env.weaponData2 = env.itemList["Weapon 2"] and env.itemList["Weapon 2"].weaponData and env.itemList["Weapon 2"].weaponData[2] or { } end -- Build and merge modifiers for allocated passives env.modDB:AddList(buildNodeModList(env, nodes, true)) -- Determine main skill group if env.mode == "CALCS" then env.calcsInput.skill_number = m_min(m_max(#build.skillsTab.socketGroupList, 1), env.calcsInput.skill_number or 1) env.mainSocketGroup = env.calcsInput.skill_number env.skillPart = env.calcsInput.skill_part or 1 env.buffMode = env.calcsInput.misc_buffMode else build.mainSocketGroup = m_min(m_max(#build.skillsTab.socketGroupList, 1), build.mainSocketGroup or 1) env.mainSocketGroup = build.mainSocketGroup env.buffMode = "EFFECTIVE" end -- Build list of active skills env.activeSkillList = { } local groupCfg = wipeTable(tempTable1) for index, socketGroup in pairs(build.skillsTab.socketGroupList) do local socketGroupSkillList = { } if socketGroup.enabled or index == env.mainSocketGroup then -- Build list of supports for this socket group local supportList = wipeTable(tempTable2) if not socketGroup.source then groupCfg.slotName = socketGroup.slot for _, value in ipairs(env.modDB:Sum("LIST", groupCfg, "ExtraSupport")) do -- Add extra supports from the item this group is socketed in local gemData = data.gems[value.name] if gemData then t_insert(supportList, { name = value.name, data = gemData, level = value.level, quality = 0, enabled = true, fromItem = true }) end end end for _, gem in ipairs(socketGroup.gemList) do if gem.enabled and gem.data and gem.data.support then -- Add support gems from this group local add = true for _, otherGem in pairs(supportList) do -- Check if there's another support with the same name already present if gem.data == otherGem.data then add = false if gem.level > otherGem.level then otherGem.level = gem.level otherGem.quality = gem.quality elseif gem.level == otherGem.level then otherGem.quality = m_max(gem.quality, otherGem.quality) end break end end if add then gem.isSupporting = { } t_insert(supportList, gem) end end end -- Create active skills for _, gem in ipairs(socketGroup.gemList) do if gem.enabled and gem.data and not gem.data.support and not gem.data.unsupported then local activeSkill = createActiveSkill(gem, supportList) activeSkill.slotName = socketGroup.slot t_insert(socketGroupSkillList, activeSkill) t_insert(env.activeSkillList, activeSkill) end end if index == env.mainSocketGroup and #socketGroupSkillList > 0 then -- Select the main skill from this socket group local activeSkillIndex if env.mode == "CALCS" then env.calcsInput.skill_activeNumber = m_min(#socketGroupSkillList, env.calcsInput.skill_activeNumber or 1) activeSkillIndex = env.calcsInput.skill_activeNumber else socketGroup.mainActiveSkill = m_min(#socketGroupSkillList, socketGroup.mainActiveSkill or 1) activeSkillIndex = socketGroup.mainActiveSkill end env.mainSkill = socketGroupSkillList[activeSkillIndex] end end if env.mode == "MAIN" then -- Create display label for the socket group if the user didn't specify one if socketGroup.label and socketGroup.label:match("%S") then socketGroup.displayLabel = socketGroup.label else socketGroup.displayLabel = nil for _, gem in ipairs(socketGroup.gemList) do if gem.enabled and gem.data and not gem.data.support then socketGroup.displayLabel = (socketGroup.displayLabel and socketGroup.displayLabel..", " or "") .. gem.name end end socketGroup.displayLabel = socketGroup.displayLabel or "" end -- Save the active skill list for display in the socket group tooltip socketGroup.displaySkillList = socketGroupSkillList end end if not env.mainSkill then -- Add a default main skill if none are specified local defaultGem = { name = "Default Attack", level = 1, quality = 0, enabled = true, data = data.gems._default } env.mainSkill = createActiveSkill(defaultGem, { }) t_insert(env.activeSkillList, env.mainSkill) end -- Build skill modifier lists env.auxSkillList = { } for _, activeSkill in pairs(env.activeSkillList) do buildActiveSkillModList(env, activeSkill) end return env end -- Finalise environment and perform the calculations -- This function is 1800 lines long. Enjoy! local function performCalcs(env) local modDB = env.modDB local enemyDB = env.enemyDB local output = { } env.output = output modDB.stats = output local breakdown if env.mode == "CALCS" then breakdown = { } env.breakdown = breakdown end -- Set modes if env.buffMode == "EFFECTIVE" then env.mode_buffs = true env.mode_combat = true env.mode_effective = true elseif env.buffMode == "COMBAT" then env.mode_buffs = true env.mode_combat = true env.mode_effective = false elseif env.buffMode == "BUFFED" then env.mode_buffs = true env.mode_combat = false env.mode_effective = false else env.mode_buffs = false env.mode_combat = false env.mode_effective = false end -- Merge keystone modifiers do local keystoneList = wipeTable(tempTable1) for _, name in ipairs(modDB:Sum("LIST", nil, "Keystone")) do keystoneList[name] = true end for name in pairs(keystoneList) do modDB:AddList(env.build.tree.keystoneMap[name].modList) end end -- Merge flask modifiers if env.mode_combat then local effectInc = modDB:Sum("INC", nil, "FlaskEffect") for item in pairs(env.flasks) do modDB.conditions["UsingFlask"] = true modDB:ScaleAddList(item.modList, 1 + (effectInc + item.flaskData.effectInc) / 100) end end -- Set conditions local condList = modDB.conditions if env.weaponData1.type == "Staff" then condList["UsingStaff"] = true end if env.weaponData1.type == "Bow" then condList["UsingBow"] = true end if env.itemList["Weapon 2"] and env.itemList["Weapon 2"].type == "Shield" then condList["UsingShield"] = true end if env.weaponData1.type and env.weaponData2.type then condList["DualWielding"] = true end if env.weaponData1.type == "None" then condList["Unarmed"] = true end if (modDB.multipliers["NormalItem"] or 0) > 0 then condList["UsingNormalItem"] = true end if (modDB.multipliers["MagicItem"] or 0) > 0 then condList["UsingMagicItem"] = true end if (modDB.multipliers["RareItem"] or 0) > 0 then condList["UsingRareItem"] = true end if (modDB.multipliers["UniqueItem"] or 0) > 0 then condList["UsingUniqueItem"] = true end if (modDB.multipliers["CorruptedItem"] or 0) > 0 then condList["UsingCorruptedItem"] = true else condList["NotUsingCorruptedItem"] = true end if env.mode_buffs then condList["Buffed"] = true end if env.mode_combat then condList["Combat"] = true if not modDB:Sum("FLAG", nil, "NeverCrit") then condList["CritInPast8Sec"] = true end if env.mainSkill.skillFlags.attack then condList["AttackedRecently"] = true elseif env.mainSkill.skillFlags.spell then condList["CastSpellRecently"] = true end if not env.mainSkill.skillFlags.trap and not env.mainSkill.skillFlags.mine and not env.mainSkill.skillFlags.totem then condList["HitRecently"] = true end if env.mainSkill.skillFlags.movement then condList["UsedMovementSkillRecently"] = true end if env.mainSkill.skillFlags.totem then condList["HaveTotem"] = true condList["SummonedTotemRecently"] = true end if env.mainSkill.skillFlags.mine then condList["DetonatedMinesRecently"] = true end end if env.mode_effective then condList["Effective"] = true end -- Check for extra curses for _, value in ipairs(modDB:Sum("LIST", nil, "ExtraCurse")) do local modList = common.New("ModList") mergeGemMods(modList, { level = value.level, quality = 0, data = data.gems[value.name], }) modDB.multipliers["CurseOnEnemy"] = (modDB.multipliers["CurseOnEnemy"] or 0) + 1 local curseModList = { } for _, mod in ipairs(modList) do for _, tag in ipairs(mod.tagList) do if tag.type == "GlobalEffect" and tag.effectType == "Curse" then t_insert(curseModList, mod) break end end end enemyDB:ScaleAddList(curseModList, (1 + enemyDB:Sum("INC", nil, "CurseEffect") / 100) * enemyDB:Sum("MORE", nil, "CurseEffect")) end -- Check for extra modifiers to apply to aura skills local extraAuraModList = { } if modDB.mods.ExtraAuraEffect then for _, mod in ipairs(modDB.mods.ExtraAuraEffect) do mod.value.source = mod.source t_insert(extraAuraModList, mod.value) end end -- Merge auxillary skill modifiers and calculate skill life and mana reservations env.reserved_LifeBase = 0 env.reserved_LifePercent = 0 env.reserved_ManaBase = 0 env.reserved_ManaPercent = 0 if breakdown then breakdown.LifeReserved = { reservations = { } } breakdown.ManaReserved = { reservations = { } } end for _, activeSkill in pairs(env.activeSkillList) do local skillModList = activeSkill.skillModList local skillCfg = activeSkill.skillCfg -- Merge auxillary modifiers if env.mode_buffs then if activeSkill.buffModList and (not activeSkill.skillFlags.totem or activeSkill.skillData.allowTotemBuff) and (not activeSkill.skillData.offering or modDB:Sum("FLAG", nil, "OfferingsAffectPlayer")) then activeSkill.buffSkill = true local inc = modDB:Sum("INC", skillCfg, "BuffEffect") if activeSkill.activeGem.data.golem and modDB:Sum("FLAG", skillCfg, "LiegeOfThePrimordial") and (activeSkill.activeGem.data.fire or activeSkill.activeGem.data.cold or activeSkill.activeGem.data.lightning) then inc = inc + 100 end modDB:ScaleAddList(activeSkill.buffModList, 1 + inc / 100) end if activeSkill.auraModList then activeSkill.buffSkill = true local inc = modDB:Sum("INC", skillCfg, "AuraEffect") + skillModList:Sum("INC", skillCfg, "AuraEffect") + modDB:Sum("INC", skillCfg, "BuffEffect") local more = modDB:Sum("MORE", skillCfg, "AuraEffect") * skillModList:Sum("MORE", skillCfg, "AuraEffect") modDB:ScaleAddList(activeSkill.auraModList, (1 + inc / 100) * more) modDB:ScaleAddList(extraAuraModList, (1 + inc / 100) * more) condList["HaveAuraActive"] = true end end if env.mode_effective then if activeSkill.debuffModList then activeSkill.debuffSkill = true enemyDB:ScaleAddList(activeSkill.debuffModList, activeSkill.skillData.stackCount or 1) end if activeSkill.curseModList then activeSkill.debuffSkill = true condList["EnemyCursed"] = true modDB.multipliers["CurseOnEnemy"] = (modDB.multipliers["CurseOnEnemy"] or 0) + 1 local inc = modDB:Sum("INC", skillCfg, "CurseEffect") + enemyDB:Sum("INC", nil, "CurseEffect") + skillModList:Sum("INC", skillCfg, "CurseEffect") local more = modDB:Sum("MORE", skillCfg, "CurseEffect") * enemyDB:Sum("MORE", nil, "CurseEffect") * skillModList:Sum("MORE", skillCfg, "CurseEffect") enemyDB:ScaleAddList(activeSkill.curseModList, (1 + inc / 100) * more) end end -- Calculate reservations if activeSkill.skillTypes[SkillType.ManaCostReserved] and not activeSkill.skillFlags.totem then local baseVal = activeSkill.skillData.manaCostOverride or activeSkill.skillData.manaCost local suffix = activeSkill.skillTypes[SkillType.ManaCostPercent] and "Percent" or "Base" local mult = skillModList:Sum("MORE", skillCfg, "ManaCost") local more = modDB:Sum("MORE", skillCfg, "ManaReserved") * skillModList:Sum("MORE", skillCfg, "ManaReserved") local inc = modDB:Sum("INC", skillCfg, "ManaReserved") + skillModList:Sum("INC", skillCfg, "ManaReserved") local base = m_floor(baseVal * mult) local cost = base - m_floor(base * -m_floor((100 + inc) * more - 100) / 100) local pool if modDB:Sum("FLAG", skillCfg, "BloodMagic", "SkillBloodMagic") or skillModList:Sum("FLAG", skillCfg, "SkillBloodMagic") then pool = "Life" else pool = "Mana" end env["reserved_"..pool..suffix] = env["reserved_"..pool..suffix] + cost if breakdown then t_insert(breakdown[pool.."Reserved"].reservations, { skillName = activeSkill.activeGem.name, base = baseVal .. (activeSkill.skillTypes[SkillType.ManaCostPercent] and "%" or ""), mult = mult ~= 1 and ("x "..mult), more = more ~= 1 and ("x "..more), inc = inc ~= 0 and ("x "..(1 + inc/100)), total = cost .. (activeSkill.skillTypes[SkillType.ManaCostPercent] and "%" or ""), }) end end end -- Process misc modifiers for _, value in ipairs(modDB:Sum("LIST", nil, "Misc")) do if value.type == "Condition" then condList[value.var] = true elseif value.type == "EnemyCondition" then enemyDB.conditions[value.var] = true elseif value.type == "Multiplier" then modDB.multipliers[value.var] = (modDB.multipliers[value.var] or 0) + value.value end end -- Process enemy modifiers last in case they depend on conditions that were set by misc modifiers for _, value in ipairs(modDB:Sum("LIST", nil, "Misc")) do if value.type == "EnemyModifier" then enemyDB:AddMod(value.mod) end end -- Process conditions that can depend on other conditions if condList["EnemyIgnited"] then condList["EnemyBurning"] = true end -- Calculate current and maximum charges output.PowerChargesMax = modDB:Sum("BASE", nil, "PowerChargesMax") output.FrenzyChargesMax = modDB:Sum("BASE", nil, "FrenzyChargesMax") output.EnduranceChargesMax = modDB:Sum("BASE", nil, "EnduranceChargesMax") if env.configInput.usePowerCharges and env.mode_combat then output.PowerCharges = output.PowerChargesMax else output.PowerCharges = 0 end if env.configInput.useFrenzyCharges and env.mode_combat then output.FrenzyCharges = output.FrenzyChargesMax else output.FrenzyCharges = 0 end if env.configInput.useEnduranceCharges and env.mode_combat then output.EnduranceCharges = output.EnduranceChargesMax else output.EnduranceCharges = 0 end modDB.multipliers["PowerCharge"] = output.PowerCharges modDB.multipliers["FrenzyCharge"] = output.FrenzyCharges modDB.multipliers["EnduranceCharge"] = output.EnduranceCharges if output.PowerCharges == output.PowerChargesMax then condList["AtMaxPowerCharges"] = true end if output.FrenzyCharges == output.FrenzyChargesMax then condList["AtMaxFrenzyCharges"] = true end if output.EnduranceCharges == output.EnduranceChargesMax then condList["AtMaxEnduranceCharges"] = true end -- Add misc buffs if env.mode_combat then if condList["Onslaught"] then local effect = m_floor(20 * (1 + modDB:Sum("INC", nil, "OnslaughtEffect", "BuffEffect") / 100)) modDB:NewMod("Speed", "INC", effect, "Onslaught") modDB:NewMod("MovementSpeed", "INC", effect, "Onslaught") end if condList["UnholyMight"] then local effect = m_floor(30 * (1 + modDB:Sum("INC", nil, "BuffEffect") / 100)) modDB:NewMod("PhysicalDamageGainAsChaos", "BASE", effect, "Unholy Might") end end -- Helper functions for stat breakdowns local simpleBreakdown, modBreakdown, slotBreakdown, effMultBreakdown, dotBreakdown if breakdown then simpleBreakdown = function(extraBase, cfg, total, ...) extraBase = extraBase or 0 local base = modDB:Sum("BASE", cfg, (...)) if (base + extraBase) ~= 0 then local inc = modDB:Sum("INC", cfg, ...) local more = modDB:Sum("MORE", cfg, ...) if inc ~= 0 or more ~= 1 or (base ~= 0 and extraBase ~= 0) then local out = { } if base ~= 0 and extraBase ~= 0 then out[1] = s_format("(%g + %g) ^8(base)", extraBase, base) else out[1] = s_format("%g ^8(base)", base + extraBase) end if inc ~= 0 then t_insert(out, s_format("x %.2f", 1 + inc/100).." ^8(increased/reduced)") end if more ~= 1 then t_insert(out, s_format("x %.2f", more).." ^8(more/less)") end t_insert(out, s_format("= %g", total)) return out end end end modBreakdown = function(cfg, ...) local inc = modDB:Sum("INC", cfg, ...) local more = modDB:Sum("MORE", cfg, ...) if inc ~= 0 and more ~= 1 then return { s_format("%.2f", 1 + inc/100).." ^8(increased/reduced)", s_format("x %.2f", more).." ^8(more/less)", s_format("= %.2f", (1 + inc/100) * more), } end end slotBreakdown = function(source, sourceName, cfg, base, total, ...) local inc = modDB:Sum("INC", cfg, ...) local more = modDB:Sum("MORE", cfg, ...) t_insert(breakdown[...].slots, { base = base, inc = (inc ~= 0) and s_format(" x %.2f", 1 + inc/100), more = (more ~= 1) and s_format(" x %.2f", more), total = s_format("%.2f", total or (base * (1 + inc / 100) * more)), source = source, sourceName = sourceName, item = env.itemList[source], }) end effMultBreakdown = function(damageType, resist, pen, taken, mult) local out = { } local resistForm = (damageType == "Physical") and "physical damage reduction" or "resistance" if resist ~= 0 then t_insert(out, s_format("Enemy %s: %d%%", resistForm, resist)) end if pen ~= 0 then t_insert(out, "Effective resistance:") t_insert(out, s_format("%d%% ^8(resistance)", resist)) t_insert(out, s_format("- %d%% ^8(penetration)", pen)) t_insert(out, s_format("= %d%%", resist - pen)) end if (resist - pen) ~= 0 and taken ~= 0 then t_insert(out, "Effective DPS modifier:") t_insert(out, s_format("%.2f ^8(%s)", 1 - (resist - pen) / 100, resistForm)) t_insert(out, s_format("x %.2f ^8(increased/reduced damage taken)", 1 + taken / 100)) t_insert(out, s_format("= %.3f", mult)) end return out end dotBreakdown = function(out, baseVal, inc, more, rate, effMult, total) t_insert(out, s_format("%.1f ^8(base damage per second)", baseVal)) if inc ~= 0 then t_insert(out, s_format("x %.2f ^8(increased/reduced)", 1 + inc/100)) end if more ~= 1 then t_insert(out, s_format("x %.2f ^8(more/less)", more)) end if rate and rate ~= 1 then t_insert(out, s_format("x %.2f ^8(rate modifier)", rate)) end if effMult ~= 1 then t_insert(out, s_format("x %.3f ^8(effective DPS modifier)", effMult)) end t_insert(out, s_format("= %.1f ^8per second", total)) end end -- Calculate attributes for _, stat in pairs({"Str","Dex","Int"}) do output[stat] = round(calcVal(modDB, stat)) if breakdown then breakdown[stat] = simpleBreakdown(nil, nil, output[stat], stat) end end -- Add attribute bonuses modDB:NewMod("Life", "BASE", m_floor(output.Str / 2), "Strength") local strDmgBonus = round((output.Str + modDB:Sum("BASE", nil, "DexIntToMeleeBonus")) / 5) modDB:NewMod("PhysicalDamage", "INC", strDmgBonus, "Strength", ModFlag.Melee) modDB:NewMod("Accuracy", "BASE", output.Dex * 2, "Dexterity") if not modDB:Sum("FLAG", nil, "IronReflexes") then modDB:NewMod("Evasion", "INC", round(output.Dex / 5), "Dexterity") end modDB:NewMod("Mana", "BASE", round(output.Int / 2), "Intelligence") modDB:NewMod("EnergyShield", "INC", round(output.Int / 5), "Intelligence") -- ---------------------- -- -- Defensive Calculations -- -- ---------------------- -- -- Life/mana pools if modDB:Sum("FLAG", nil, "ChaosInoculation") then output.Life = 1 condList["FullLife"] = true else local base = modDB:Sum("BASE", cfg, "Life") local inc = modDB:Sum("INC", cfg, "Life") local more = modDB:Sum("MORE", cfg, "Life") local conv = modDB:Sum("BASE", nil, "LifeConvertToEnergyShield") output.Life = round(base * (1 + inc/100) * more * (1 - conv/100)) if breakdown then if inc ~= 0 or more ~= 1 or conv ~= 0 then breakdown.Life = { } breakdown.Life[1] = s_format("%g ^8(base)", base) if inc ~= 0 then t_insert(breakdown.Life, s_format("x %.2f ^8(increased/reduced)", 1 + inc/100)) end if more ~= 1 then t_insert(breakdown.Life, s_format("x %.2f ^8(more/less)", more)) end if conv ~= 0 then t_insert(breakdown.Life, s_format("x %.2f ^8(converted to Energy Shield)", 1 - conv/100)) end t_insert(breakdown.Life, s_format("= %g", output.Life)) end end end output.Mana = round(calcVal(modDB, "Mana")) output.ManaRegen = round((modDB:Sum("BASE", nil, "ManaRegen") + output.Mana * modDB:Sum("BASE", nil, "ManaRegenPercent") / 100) * calcMod(modDB, nil, "ManaRegen", "ManaRecovery"), 1) if breakdown then breakdown.Mana = simpleBreakdown(nil, nil, output.Mana, "Mana") breakdown.ManaRegen = simpleBreakdown(nil, nil, output.ManaRegen, "ManaRegen", "ManaRecovery") end -- Life/mana reservation for _, pool in pairs({"Life", "Mana"}) do local max = output[pool] local reserved = env["reserved_"..pool.."Base"] + m_ceil(max * env["reserved_"..pool.."Percent"] / 100) output[pool.."Reserved"] = reserved output[pool.."ReservedPercent"] = reserved / max * 100 output[pool.."Unreserved"] = max - reserved output[pool.."UnreservedPercent"] = (max - reserved) / max * 100 if (max - reserved) / max <= 0.35 then condList["Low"..pool] = true end if reserved == 0 then condList["No"..pool.."Reserved"] = true end end -- Resistances for _, elem in ipairs(resistTypeList) do local max, total if elem == "Chaos" and modDB:Sum("FLAG", nil, "ChaosInoculation") then max = 100 total = 100 else max = modDB:Sum("BASE", nil, elem.."ResistMax") total = modDB:Sum("BASE", nil, elem.."Resist", isElemental[elem] and "ElementalResist") end output[elem.."Resist"] = m_min(total, max) output[elem.."ResistTotal"] = total output[elem.."ResistOverCap"] = m_max(0, total - max) if breakdown then breakdown[elem.."Resist"] = { "Max: "..max.."%", "Total: "..total.."%", "In hideout: "..(total + 60).."%", } end end -- Primary defences: Energy shield, evasion and armour do local ironReflexes = modDB:Sum("FLAG", nil, "IronReflexes") local energyShield = 0 local armour = 0 local evasion = 0 if breakdown then breakdown.EnergyShield = { slots = { } } breakdown.Armour = { slots = { } } breakdown.Evasion = { slots = { } } end local energyShieldBase = modDB:Sum("BASE", nil, "EnergyShield") if energyShieldBase > 0 then energyShield = energyShield + energyShieldBase * calcMod(modDB, nil, "EnergyShield", "Defences") if breakdown then slotBreakdown("Global", nil, nil, energyShieldBase, nil, "EnergyShield", "Defences") end end local armourBase = modDB:Sum("BASE", nil, "Armour", "ArmourAndEvasion") if armourBase > 0 then armour = armour + armourBase * calcMod(modDB, nil, "Armour", "ArmourAndEvasion", "Defences") if breakdown then slotBreakdown("Global", nil, nil, armourBase, nil, "Armour", "ArmourAndEvasion", "Defences") end end local evasionBase = modDB:Sum("BASE", nil, "Evasion", "ArmourAndEvasion") if evasionBase > 0 then if ironReflexes then armour = armour + evasionBase * calcMod(modDB, nil, "Armour", "Evasion", "ArmourAndEvasion", "Defences") if breakdown then slotBreakdown("Conversion", "Evasion to Armour", nil, evasionBase, nil, "Armour", "Evasion", "ArmourAndEvasion", "Defences") end else evasion = evasion + evasionBase * calcMod(modDB, nil, "Evasion", "ArmourAndEvasion", "Defences") if breakdown then slotBreakdown("Global", nil, nil, evasionBase, nil, "Evasion", "ArmourAndEvasion", "Defences") end end end local gearEnergyShield = 0 local gearArmour = 0 local gearEvasion = 0 local slotCfg = wipeTable(tempTable1) for _, slot in pairs({"Helmet","Body Armour","Gloves","Boots","Weapon 2"}) do local armourData = env.itemList[slot] and env.itemList[slot].armourData if armourData then slotCfg.slotName = slot energyShieldBase = armourData.EnergyShield or 0 if energyShieldBase > 0 then energyShield = energyShield + energyShieldBase * calcMod(modDB, slotCfg, "EnergyShield", "Defences") gearEnergyShield = gearEnergyShield + energyShieldBase if breakdown then slotBreakdown(slot, nil, slotCfg, energyShieldBase, nil, "EnergyShield", "Defences") end end armourBase = armourData.Armour or 0 if armourBase > 0 then if slot == "Body Armour" and modDB:Sum("FLAG", nil, "Unbreakable") then armourBase = armourBase * 2 end armour = armour + armourBase * calcMod(modDB, slotCfg, "Armour", "ArmourAndEvasion", "Defences") gearArmour = gearArmour + armourBase if breakdown then slotBreakdown(slot, nil, slotCfg, armourBase, nil, "Armour", "ArmourAndEvasion", "Defences") end end evasionBase = armourData.Evasion or 0 if evasionBase > 0 then if ironReflexes then armour = armour + evasionBase * calcMod(modDB, slotCfg, "Armour", "Evasion", "ArmourAndEvasion", "Defences") gearArmour = gearArmour + evasionBase if breakdown then slotBreakdown(slot, nil, slotCfg, evasionBase, nil, "Armour", "Evasion", "ArmourAndEvasion", "Defences") end else evasion = evasion + evasionBase * calcMod(modDB, slotCfg, "Evasion", "ArmourAndEvasion", "Defences") gearEvasion = gearEvasion + evasionBase if breakdown then slotBreakdown(slot, nil, slotCfg, evasionBase, nil, "Evasion", "ArmourAndEvasion", "Defences") end end end end end local convManaToES = modDB:Sum("BASE", nil, "ManaGainAsEnergyShield") if convManaToES > 0 then energyShieldBase = modDB:Sum("BASE", nil, "Mana") * convManaToES / 100 energyShield = energyShield + energyShieldBase * calcMod(modDB, nil, "Mana", "EnergyShield", "Defences") if breakdown then slotBreakdown("Conversion", "Mana to Energy Shield", nil, energyShieldBase, nil, "EnergyShield", "Defences", "Mana") end end local convLifeToES = modDB:Sum("BASE", nil, "LifeConvertToEnergyShield", "LifeGainAsEnergyShield") if convLifeToES > 0 then energyShieldBase = modDB:Sum("BASE", nil, "Life") * convLifeToES / 100 local total if modDB:Sum("FLAG", nil, "ChaosInoculation") then total = 1 else total = energyShieldBase * calcMod(modDB, nil, "Life", "EnergyShield", "Defences") end energyShield = energyShield + total if breakdown then slotBreakdown("Conversion", "Life to Energy Shield", nil, energyShieldBase, total, "EnergyShield", "Defences", "Life") end end output.EnergyShield = round(energyShield) output.Armour = round(armour) output.Evasion = round(evasion) output.LowestOfArmourAndEvasion = m_min(output.Armour, output.Evasion) output["Gear:EnergyShield"] = gearEnergyShield output["Gear:Armour"] = gearArmour output["Gear:Evasion"] = gearEvasion output.EnergyShieldRecharge = round(output.EnergyShield * 0.2 * calcMod(modDB, nil, "EnergyShieldRecharge", "EnergyShieldRecovery"), 1) output.EnergyShieldRechargeDelay = 2 / (1 + modDB:Sum("INC", nil, "EnergyShieldRechargeFaster") / 100) if breakdown then breakdown.EnergyShieldRecharge = simpleBreakdown(output.EnergyShield * 0.2, nil, output.EnergyShieldRecharge, "EnergyShieldRecharge", "EnergyShieldRecovery") if output.EnergyShieldRechargeDelay ~= 2 then breakdown.EnergyShieldRechargeDelay = { "2.00s ^8(base)", s_format("/ %.2f ^8(faster start)", 1 + modDB:Sum("INC", nil, "EnergyShieldRechargeFaster") / 100), s_format("= %.2fs", output.EnergyShieldRechargeDelay) } end end if modDB:Sum("FLAG", nil, "CannotEvade") then output.EvadeChance = 0 else local enemyAccuracy = round(calcVal(enemyDB, "Accuracy")) output.EvadeChance = 100 - calcHitChance(output.Evasion, enemyAccuracy) * calcMod(enemyDB, nil, "HitChance") if breakdown then breakdown.EvadeChance = { s_format("Enemy level: %d ^8(%s the Configuration tab)", env.enemyLevel, env.configInput.enemyLevel and "overridden from" or "can be overridden in"), s_format("Average enemy accuracy: %d", enemyAccuracy), s_format("Approximate evade chance: %d%%", output.EvadeChance), } end end end -- Life and energy shield regen do if modDB:Sum("FLAG", nil, "NoLifeRegen") then output.LifeRegen = 0 elseif modDB:Sum("FLAG", nil, "ZealotsOath") then output.LifeRegen = 0 local lifeBase = modDB:Sum("BASE", nil, "LifeRegen") if lifeBase > 0 then modDB:NewMod("EnergyShieldRegen", "BASE", lifeBase, "Zealot's Oath") end local lifePercent = modDB:Sum("BASE", nil, "LifeRegenPercent") if lifePercent > 0 then modDB:NewMod("EnergyShieldRegenPercent", "BASE", lifePercent, "Zealot's Oath") end else local lifeBase = modDB:Sum("BASE", nil, "LifeRegen") local lifePercent = modDB:Sum("BASE", nil, "LifeRegenPercent") if lifePercent > 0 then lifeBase = lifeBase + output.Life * lifePercent / 100 end if lifeBase > 0 then output.LifeRegen = lifeBase * calcMod(modDB, nil, "LifeRecovery") output.LifeRegenPercent = round(lifeBase / output.Life * 100, 1) else output.LifeRegen = 0 end end local esBase = modDB:Sum("BASE", nil, "EnergyShieldRegen") local esPercent = modDB:Sum("BASE", nil, "EnergyShieldRegenPercent") if esPercent > 0 then esBase = esBase + output.EnergyShield * esPercent / 100 end if esBase > 0 then output.EnergyShieldRegen = esBase * calcMod(modDB, nil, "EnergyShieldRecovery") output.EnergyShieldRegenPercent = round(esBase / output.EnergyShield * 100, 1) else output.EnergyShieldRegen = 0 end end -- Other defences: block, dodge, stun recovery/avoidance do output.MovementSpeedMod = calcMod(modDB, nil, "MovementSpeed") if modDB:Sum("FLAG", nil, "MovementSpeedCannotBeBelowBase") then output.MovementSpeedMod = m_max(output.MovementSpeedMod, 1) end output.BlockChanceMax = modDB:Sum("BASE", nil, "BlockChanceMax") local shieldData = env.itemList["Weapon 2"] and env.itemList["Weapon 2"].armourData output.BlockChance = m_min(((shieldData and shieldData.BlockChance or 0) + modDB:Sum("BASE", nil, "BlockChance")) * calcMod(modDB, nil, "BlockChance"), output.BlockChanceMax) output.SpellBlockChance = m_min(modDB:Sum("BASE", nil, "SpellBlockChance") * calcMod(modDB, nil, "SpellBlockChance") + output.BlockChance * modDB:Sum("BASE", nil, "BlockChanceConv") / 100, output.BlockChanceMax) if breakdown then breakdown.BlockChance = simpleBreakdown(shieldData and shieldData.BlockChance, nil, output.BlockChance, "BlockChance") breakdown.SpellBlockChance = simpleBreakdown(output.BlockChance * modDB:Sum("BASE", nil, "BlockChanceConv") / 100, nil, output.SpellBlockChance, "SpellBlockChance") end if modDB:Sum("FLAG", nil, "CannotBlockAttacks") then output.BlockChance = 0 end output.AttackDodgeChance = m_min(modDB:Sum("BASE", nil, "AttackDodgeChance"), 75) output.SpellDodgeChance = m_min(modDB:Sum("BASE", nil, "SpellDodgeChance"), 75) local stunChance = 100 - modDB:Sum("BASE", nil, "AvoidStun") if output.EnergyShield > output.Life * 2 then stunChance = stunChance * 0.5 end output.StunAvoidChance = 100 - stunChance if output.StunAvoidChance >= 100 then output.StunDuration = 0 output.BlockDuration = 0 else output.StunDuration = 0.35 / (1 + modDB:Sum("INC", nil, "StunRecovery") / 100) output.BlockDuration = 0.35 / (1 + modDB:Sum("INC", nil, "StunRecovery", "BlockRecovery") / 100) if breakdown then breakdown.StunDuration = { "0.35s ^8(base)", s_format("/ %.2f ^8(increased/reduced recovery)", 1 + modDB:Sum("INC", nil, "StunRecovery") / 100), s_format("= %.2fs", output.StunDuration) } breakdown.BlockDuration = { "0.35s ^8(base)", s_format("/ %.2f ^8(increased/reduced recovery)", 1 + modDB:Sum("INC", nil, "StunRecovery", "BlockRecovery") / 100), s_format("= %.2fs", output.BlockDuration) } end end end -- ---------------------- -- -- Offensive Calculations -- -- ---------------------- -- if env.mainSkill.skillFlags.disable then -- Skill is disabled output.CombinedDPS = 0 return end -- Merge main skill mods modDB:AddList(env.mainSkill.skillModList) local skillData = env.mainSkill.skillData local skillFlags = env.mainSkill.skillFlags local skillCfg = env.mainSkill.skillCfg if env.mainSkill.skillFlags.attack then env.mode_skillType = "ATTACK" else env.mode_skillType = "SPELL" end if env.mainSkill.skillData.showAverage then env.mode_average = true else skillFlags.notAverage = true end if env.mode_buffs then skillFlags.buffs = true end if env.mode_combat then skillFlags.combat = true end if env.mode_effective then skillFlags.effective = true end -- Update skill data for _, value in ipairs(modDB:Sum("LIST", skillCfg, "Misc")) do if value.type == "SkillData" then 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 end env.modDB.conditions["SkillIsTriggered"] = skillData.triggered -- Add addition stat bonuses if modDB:Sum("FLAG", nil, "IronGrip") then modDB:NewMod("PhysicalDamage", "INC", strDmgBonus, "Strength", bor(ModFlag.Attack, ModFlag.Projectile)) end if modDB:Sum("FLAG", nil, "IronWill") then modDB:NewMod("Damage", "INC", strDmgBonus, "Strength", ModFlag.Spell) end if modDB:Sum("FLAG", nil, "MinionDamageAppliesToPlayer") then -- Minion Damage conversion from The Scourge for _, mod in ipairs(modDB.mods.Damage or { }) do if mod.type == "INC" and mod.keywordFlags == KeywordFlag.Minion then modDB:NewMod("Damage", "INC", mod.value, mod.source, 0, 0, unpack(mod.tagList)) end end end if modDB:Sum("FLAG", nil, "SpellDamageAppliesToAttacks") then -- Spell Damage conversion from Crown of Eyes for i, mod in ipairs(modDB.mods.Damage or { }) do if mod.type == "INC" and band(mod.flags, ModFlag.Spell) ~= 0 then modDB:NewMod("Damage", "INC", mod.value, mod.source, bor(band(mod.flags, bnot(ModFlag.Spell)), ModFlag.Attack), mod.keywordFlags, unpack(mod.tagList)) end end end local isAttack = (env.mode_skillType == "ATTACK") -- Calculate skill type stats if skillFlags.projectile then output.ProjectileCount = modDB:Sum("BASE", skillCfg, "ProjectileCount") output.PierceChance = m_min(100, modDB:Sum("BASE", skillCfg, "PierceChance")) output.ProjectileSpeedMod = calcMod(modDB, skillCfg, "ProjectileSpeed") if breakdown then breakdown.ProjectileSpeedMod = modBreakdown(skillCfg, "ProjectileSpeed") end end if skillFlags.area then output.AreaRadiusMod = calcMod(modDB, skillCfg, "AreaRadius") if breakdown then breakdown.AreaRadiusMod = modBreakdown(skillCfg, "AreaRadius") end end if skillFlags.trap then output.ActiveTrapLimit = modDB:Sum("BASE", skillCfg, "ActiveTrapLimit") output.TrapCooldown = (skillData.trapCooldown or 4) / calcMod(modDB, skillCfg, "TrapCooldownRecovery") if breakdown then breakdown.TrapCooldown = { s_format("%.2fs ^8(base)", skillData.trapCooldown or 4), s_format("/ %.2f ^8(increased/reduced cooldown recovery)", 1 + modDB:Sum("INC", skillCfg, "TrapCooldownRecovery") / 100), s_format("= %.2fs", output.TrapCooldown) } end end if skillFlags.mine then output.ActiveMineLimit = modDB:Sum("BASE", skillCfg, "ActiveMineLimit") end if skillFlags.totem then output.ActiveTotemLimit = modDB:Sum("BASE", skillCfg, "ActiveTotemLimit") output.TotemLifeMod = calcMod(modDB, skillCfg, "TotemLife") output.TotemLife = round(data.monsterLifeTable[skillData.totemLevel] * data.totemLifeMult[env.mainSkill.skillTotemId] * output.TotemLifeMod) if breakdown then breakdown.TotemLifeMod = modBreakdown(skillCfg, "TotemLife") breakdown.TotemLife = { "Totem level: "..skillData.totemLevel, data.monsterLifeTable[skillData.totemLevel].." ^8(base life for a level "..skillData.totemLevel.." monster)", "x "..data.totemLifeMult[env.mainSkill.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 / calcMod(enemyDB, skillCfg, "BuffExpireFaster") else debuffDurationMult = 1 end do output.DurationMod = calcMod(modDB, skillCfg, "Duration") if breakdown then breakdown.DurationMod = modBreakdown(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 = env.mainSkill.activeGem.data.setupFunc if setupFunc then setupFunc(env, output) end end -- Cache global damage disabling flags local canDeal = { } for _, damageType in pairs(dmgTypeList) do canDeal[damageType] = not modDB:Sum("FLAG", skillCfg, "DealNo"..damageType) end -- Calculate damage conversion percentages env.conversionTable = wipeTable(env.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] = modDB:Sum("BASE", skillCfg, damageType.."DamageConvertTo"..otherType, isElemental[damageType] and "ElementalDamageConvertTo"..otherType or nil) globalTotal = globalTotal + globalConv[otherType] skillConv[otherType] = modDB:Sum("BASE", skillCfg, "Skill"..damageType.."DamageConvertTo"..otherType) skillTotal = skillTotal + skillConv[otherType] add[otherType] = modDB: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) env.conversionTable[damageType] = dmgTable end env.conversionTable["Chaos"] = { mult = 1 } -- Calculate mana cost (may be slightly off due to rounding differences) do local more = m_floor(modDB:Sum("MORE", skillCfg, "ManaCost") * 100 + 0.0001) / 100 local inc = modDB:Sum("INC", skillCfg, "ManaCost") local base = modDB:Sum("BASE", skillCfg, "ManaCost") output.ManaCost = m_floor(m_max(0, (skillData.manaCost or 0) * more * (1 + inc / 100) + base)) if env.mainSkill.skillTypes[SkillType.ManaCostPercent] and skillFlags.totem then output.ManaCost = m_floor(output.Mana * output.ManaCost / 100) end if breakdown and output.ManaCost ~= (skillData.manaCost or 0) then breakdown.ManaCost = { s_format("%d ^8(base mana cost)", skillData.manaCost or 0) } 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 = { } end env.mainSkill.weapon1Cfg.skillStats = output.MainHand t_insert(passList, { label = "Main Hand", source = env.weaponData1, cfg = env.mainSkill.weapon1Cfg, output = output.MainHand, breakdown = breakdown and breakdown.MainHand, }) end if skillFlags.weapon2Attack then if breakdown then breakdown.OffHand = { } end env.mainSkill.weapon2Cfg.skillStats = output.OffHand t_insert(passList, { label = "Off Hand", source = env.weaponData2, cfg = env.mainSkill.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 source, output, cfg, breakdown = pass.source, pass.output, pass.cfg, pass.breakdown -- Calculate hit chance output.Accuracy = calcVal(modDB, "Accuracy", cfg) if breakdown then breakdown.Accuracy = simpleBreakdown(nil, cfg, output.Accuracy, "Accuracy") end if not isAttack or modDB:Sum("FLAG", cfg, "CannotBeEvaded") or skillData.cannotBeEvaded then output.HitChance = 100 else local enemyEvasion = round(calcVal(enemyDB, "Evasion")) output.HitChance = calcHitChance(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 output.Speed = baseSpeed * round(calcMod(modDB, cfg, "Speed"), 2) output.Time = 1 / output.Speed if breakdown then breakdown.Speed = simpleBreakdown(baseSpeed, cfg, output.Speed, "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") output.Time = 1 / output.Speed 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 modDB:Sum("FLAG", nil, "NeverCrit") then output.CritChance = 0 output.CritMultiplier = 0 output.CritEffect = 1 else local baseCrit = source.critChance or 0 if baseCrit == 100 then output.CritChance = 100 else local base = modDB:Sum("BASE", cfg, "CritChance") local inc = modDB:Sum("INC", cfg, "CritChance") local more = modDB:Sum("MORE", cfg, "CritChance") output.CritChance = (baseCrit + base) * (1 + inc / 100) * more if env.mode_effective then output.CritChance = output.CritChance + enemyDB:Sum("BASE", nil, "SelfExtraCritChance") end local preCapCritChance = output.CritChance output.CritChance = m_min(output.CritChance, 95) if (baseCrit + base) > 0 then output.CritChance = m_max(output.CritChance, 5) end local preLuckyCritChance = output.CritChance if env.mode_effective and modDB:Sum("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 local enemyExtra = enemyDB:Sum("BASE", nil, "SelfExtraCritChance") 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 if env.mode_effective and enemyExtra ~= 0 then t_insert(breakdown.CritChance, s_format("+ %g ^8(extra chance for enemy to be crit)", enemyExtra)) end t_insert(breakdown.CritChance, s_format("= %g", preLuckyCritChance)) 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 env.mode_effective and modDB:Sum("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 modDB:Sum("FLAG", cfg, "NoCritMultiplier") then output.CritMultiplier = 1 else local extraDamage = 0.5 + modDB:Sum("BASE", cfg, "CritMultiplier") / 100 if env.mode_effective then extraDamage = round(extraDamage * (1 + enemyDB:Sum("INC", nil, "SelfCritMultiplier") / 100), 2) end output.CritMultiplier = 1 + m_max(0, extraDamage) if breakdown and output.CritMultiplier ~= 1.5 then breakdown.CritMultiplier = { "50% ^8(base)", } local base = modDB:Sum("BASE", cfg, "CritMultiplier") if base ~= 0 then t_insert(breakdown.CritMultiplier, s_format("+ %d%% ^8(additional extra damage)", base)) end local enemyInc = 1 + enemyDB:Sum("INC", nil, "SelfCritMultiplier") / 100 if env.mode_effective and enemyInc ~= 1 then t_insert(breakdown.CritMultiplier, s_format("x %.2f ^8(increased/reduced extra crit damage taken by enemy)", enemyInc)) end t_insert(breakdown.CritMultiplier, s_format("= %d%% ^8(extra crit damage)", extraDamage * 100)) end 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 for pass = 1, 2 do -- Pass 1 is critical strike damage, pass 2 is non-critical strike condList["CriticalStrike"] = (pass == 1) for _, damageType in ipairs(dmgTypeList) do local min, max if skillFlags.hit and canDeal[damageType] then if breakdown then breakdown[damageType] = { damageComponents = { } } end min, max = calcHitDamage(env, source, cfg, breakdown and breakdown[damageType], damageType) local convMult = env.conversionTable[damageType].mult 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 end min = min * convMult max = max * convMult 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 preMult local resist = 0 local pen = 0 local taken = enemyDB:Sum("INC", nil, "DamageTaken", damageType.."DamageTaken") if damageType == "Physical" then resist = enemyDB:Sum("INC", nil, "PhysicalDamageReduction") else resist = enemyDB:Sum("BASE", nil, damageType.."Resist") if isElemental[damageType] then resist = resist + enemyDB:Sum("BASE", nil, "ElementalResist") pen = modDB: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 modDB:Sum("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"] = effMultBreakdown(damageType, resist, pen, taken, effMult) end end if breakdown then t_insert(breakdown[damageType], s_format("= %d to %d", min, max)) 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 end output.TotalMin = totalHitMin output.TotalMax = totalHitMax if 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 -- 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("CritChance", "AVERAGE") combineStat("CritMultiplier", "AVERAGE") combineStat("AverageDamage", "DPS") combineStat("TotalDPS", "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 env.mode_average 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 skill DOT components local dotCfg = { skillName = skillCfg.skillName, skillPart = skillCfg.skillPart, slotName = skillCfg.slotName, flags = bor(band(skillCfg.flags, ModFlag.SourceMask), ModFlag.Dot, skillData.dotIsSpell and ModFlag.Spell or 0), keywordFlags = skillCfg.keywordFlags } env.mainSkill.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", damageType.."DamageTaken", "DotTaken") if damageType == "Physical" then resist = enemyDB:Sum("INC", 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 if damageType == "Fire" then taken = taken + enemyDB:Sum("INC", nil, "BurningDamageTaken") 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"] = effMultBreakdown(damageType, resist, 0, taken, effMult) end end local inc = modDB:Sum("INC", dotCfg, "Damage", damageType.."Damage", isElemental[damageType] and "ElementalDamage" or nil) local more = round(modDB:Sum("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"] = { } dotBreakdown(breakdown[damageType.."Dot"], baseVal, inc, more, nil, effMult, total) end end end skillFlags.bleed = false skillFlags.poison = false skillFlags.ignite = false skillFlags.igniteCanStack = modDB:Sum("FLAG", nil, "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 condList["CriticalStrike"] = true if modDB:Sum("FLAG", cfg, "CannotBleed") then output.BleedChanceOnCrit = 0 else output.BleedChanceOnCrit = m_min(100, modDB:Sum("BASE", cfg, "BleedChance")) end output.PoisonChanceOnCrit = m_min(100, modDB:Sum("BASE", cfg, "PoisonChance")) if modDB:Sum("FLAG", cfg, "CannotIgnite") then output.IgniteChanceOnCrit = 0 else output.IgniteChanceOnCrit = 100 end if modDB:Sum("FLAG", cfg, "CannotShock") then output.ShockChanceOnCrit = 0 else output.ShockChanceOnCrit = 100 end if modDB:Sum("FLAG", cfg, "CannotFreeze") then output.FreezeChanceOnCrit = 0 else output.FreezeChanceOnCrit = 100 end condList["CriticalStrike"] = false if modDB:Sum("FLAG", cfg, "CannotBleed") then output.BleedChanceOnHit = 0 else output.BleedChanceOnHit = m_min(100, modDB:Sum("BASE", cfg, "BleedChance")) end output.PoisonChanceOnHit = m_min(100, modDB:Sum("BASE", cfg, "PoisonChance")) if modDB:Sum("FLAG", cfg, "CannotIgnite") then output.IgniteChanceOnHit = 0 else output.IgniteChanceOnHit = m_min(100, modDB:Sum("BASE", cfg, "EnemyIgniteChance") + enemyDB:Sum("BASE", nil, "SelfIgniteChance")) end if modDB:Sum("FLAG", cfg, "CannotShock") then output.ShockChanceOnHit = 0 else output.ShockChanceOnHit = m_min(100, modDB:Sum("BASE", cfg, "EnemyShockChance") + enemyDB:Sum("BASE", nil, "SelfShockChance")) end if modDB:Sum("FLAG", cfg, "CannotFreeze") then output.FreezeChanceOnHit = 0 else output.FreezeChanceOnHit = m_min(100, modDB:Sum("BASE", cfg, "EnemyFreezeChance") + enemyDB:Sum("BASE", nil, "SelfFreezeChance")) if modDB:Sum("FLAG", cfg, "CritsDontAlwaysFreeze") then output.FreezeChanceOnCrit = output.FreezeChanceOnHit end 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 baseVal = calcSecondaryEffectBase("Bleed", sourceHitDmg, sourceCritDmg) * 0.1 if baseVal > 0 then skillFlags.bleed = true skillFlags.duration = true if not env.mainSkill.bleedCfg then env.mainSkill.bleedCfg = { 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 = env.mainSkill.bleedCfg local effMult = 1 if env.mode_effective then local resist = enemyDB:Sum("INC", nil, "PhysicalDamageReduction") local taken = enemyDB:Sum("INC", dotCfg, "DamageTaken", "PhysicalDamageTaken", "DotTaken") effMult = (1 - resist / 100) * (1 + taken / 100) globalOutput["BleedEffMult"] = effMult if breakdown and effMult ~= 1 then globalBreakdown.BleedEffMult = effMultBreakdown("Physical", resist, 0, taken, effMult) end end local inc = modDB:Sum("INC", dotCfg, "Damage", "PhysicalDamage") local more = round(modDB:Sum("MORE", dotCfg, "Damage", "PhysicalDamage"), 2) output.BleedDPS = baseVal * (1 + inc/100) * more * effMult local durationMod = calcMod(modDB, dotCfg, "Duration") * calcMod(enemyDB, nil, "SelfBleedDuration") globalOutput.BleedDuration = 5 * durationMod * debuffDurationMult if breakdown then t_insert(breakdown.BleedDPS, "x 0.1 ^8(bleed deals 10% per second)") t_insert(breakdown.BleedDPS, s_format("= %.1f", baseVal)) t_insert(breakdown.BleedDPS, "Bleed DPS:") dotBreakdown(breakdown.BleedDPS, baseVal, inc, more, 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) > 0 then local sourceHitDmg = output.PhysicalHitAverage + output.ChaosHitAverage local sourceCritDmg = output.PhysicalCritAverage + output.ChaosCritAverage local baseVal = calcSecondaryEffectBase("Poison", sourceHitDmg, sourceCritDmg * modDB:Sum("MORE", cfg, "PoisonDamageOnCrit")) * 0.08 if baseVal > 0 then skillFlags.poison = true skillFlags.duration = true if not env.mainSkill.poisonCfg then env.mainSkill.poisonCfg = { 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 = env.mainSkill.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", "ChaosDamageTaken", "DotTaken") effMult = (1 - resist / 100) * (1 + taken / 100) globalOutput["PoisonEffMult"] = effMult if breakdown and effMult ~= 1 then globalBreakdown.PoisonEffMult = effMultBreakdown("Chaos", resist, 0, taken, effMult) end end local inc = modDB:Sum("INC", dotCfg, "Damage", "ChaosDamage") local more = round(modDB:Sum("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 = calcMod(modDB, dotCfg, "Duration") * calcMod(enemyDB, nil, "SelfPoisonDuration") globalOutput.PoisonDuration = durationBase * durationMod * debuffDurationMult output.PoisonDamage = output.PoisonDPS * globalOutput.PoisonDuration if env.mode_average then output.TotalPoisonAverageDamage = output.HitChance / 100 * output.PoisonChance / 100 * output.PoisonDamage else output.TotalPoisonDPS = output.HitChance / 100 * output.PoisonChance / 100 * output.PoisonDamage * (globalOutput.HitSpeed or globalOutput.Speed) * (skillData.dpsMultiplier or 1) 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:") dotBreakdown(breakdown.PoisonDPS, baseVal, inc, more, 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)) 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.Fire and not modDB:Sum("FLAG", cfg, "FireCannotIgnite") then sourceHitDmg = sourceHitDmg + output.FireHitAverage sourceCritDmg = sourceCritDmg + output.FireCritAverage end if canDeal.Cold and modDB:Sum("FLAG", cfg, "ColdCanIgnite") then sourceHitDmg = sourceHitDmg + output.ColdHitAverage sourceCritDmg = sourceCritDmg + output.ColdCritAverage 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 env.mainSkill.igniteCfg then env.mainSkill.igniteCfg = { 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 = env.mainSkill.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", "FireDamageTaken", "ElementalDamageTaken", "BurningDamageTaken", "DotTaken") effMult = (1 - resist / 100) * (1 + taken / 100) globalOutput["IgniteEffMult"] = effMult if breakdown and effMult ~= 1 then globalBreakdown.IgniteEffMult = effMultBreakdown("Fire", resist, 0, taken, effMult) end end local inc = modDB:Sum("INC", dotCfg, "Damage", "FireDamage", "ElementalDamage") local more = round(modDB:Sum("MORE", dotCfg, "Damage", "FireDamage", "ElementalDamage"), 2) local burnRateMod = calcMod(modDB, cfg, "IgniteBurnRate") output.IgniteDPS = baseVal * (1 + inc/100) * more * burnRateMod * effMult local incDur = modDB:Sum("INC", dotCfg, "EnemyIgniteDuration") + enemyDB:Sum("INC", nil, "SelfIgniteDuration") local moreDur = enemyDB:Sum("MORE", nil, "SelfIgniteDuration") globalOutput.IgniteDuration = 4 * (1 + incDur / 100) * moreDur / burnRateMod * debuffDurationMult if skillFlags.igniteCanStack then output.IgniteDamage = output.IgniteDPS * globalOutput.IgniteDuration if env.mode_average then output.TotalIgniteAverageDamage = output.HitChance / 100 * output.IgniteChance / 100 * output.IgniteDamage else output.TotalIgniteDPS = output.HitChance / 100 * output.IgniteChance / 100 * output.IgniteDamage * (globalOutput.HitSpeed or globalOutput.Speed) * (skillData.dpsMultiplier or 1) 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:") dotBreakdown(breakdown.IgniteDPS, baseVal, inc, more, 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)) 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.Lightning and not modDB:Sum("FLAG", cfg, "LightningCannotShock") then sourceHitDmg = sourceHitDmg + output.LightningHitAverage sourceCritDmg = sourceCritDmg + output.LightningCritAverage end if canDeal.Physical and modDB:Sum("FLAG", cfg, "PhysicalCanShock") then sourceHitDmg = sourceHitDmg + output.PhysicalHitAverage sourceCritDmg = sourceCritDmg + output.PhysicalCritAverage end if canDeal.Fire and modDB:Sum("FLAG", cfg, "FireCanShock") then sourceHitDmg = sourceHitDmg + output.FireHitAverage sourceCritDmg = sourceCritDmg + output.FireCritAverage end if canDeal.Chaos and modDB:Sum("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 + modDB: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 modDB:Sum("FLAG", cfg, "ColdCannotFreeze") then sourceHitDmg = sourceHitDmg + output.ColdHitAverage sourceCritDmg = sourceCritDmg + output.ColdCritAverage end if canDeal.Lightning and modDB:Sum("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 + modDB: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 enemy stun modifiers local enemyStunThresholdRed = -modDB: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 = modDB: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 env.mode_average then combineStat("TotalPoisonAverageDamage", "DPS") else combineStat("TotalPoisonDPS", "DPS") end combineStat("IgniteChance", "AVERAGE") combineStat("IgniteDPS", "CHANCE", "IgniteChance") if skillFlags.igniteCanStack then combineStat("IgniteDamage", "CHANCE", "IgniteChance") if env.mode_average then combineStat("TotalIgniteAverageDamage", "DPS") else 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 env.mainSkill.decayCfg = { slotName = skillCfg.slotName, flags = bor(band(skillCfg.flags, ModFlag.SourceMask), ModFlag.Dot, skillData.dotIsSpell and ModFlag.Spell or 0), keywordFlags = skillCfg.keywordFlags, } local dotCfg = env.mainSkill.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", "ChaosDamageTaken", "DotTaken") effMult = (1 - resist / 100) * (1 + taken / 100) output["DecayEffMult"] = effMult if breakdown and effMult ~= 1 then breakdown.DecayEffMult = effMultBreakdown("Chaos", resist, 0, taken, effMult) end end local inc = modDB:Sum("INC", dotCfg, "Damage", "ChaosDamage") local more = round(modDB:Sum("MORE", dotCfg, "Damage", "ChaosDamage"), 2) output.DecayDPS = skillData.decay * (1 + inc/100) * more * effMult local durationMod = calcMod(modDB, dotCfg, "Duration") output.DecayDuration = 10 * durationMod * debuffDurationMult if breakdown then breakdown.DecayDPS = { } t_insert(breakdown.DecayDPS, "Decay DPS:") dotBreakdown(breakdown.DecayDPS, skillData.decay, inc, more, 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[(env.mode_average and "AverageDamage") or "TotalDPS"] + output.TotalDot output.CombinedDPS = baseDPS if skillFlags.poison then if env.mode_average then output.CombinedDPS = output.CombinedDPS + output.TotalPoisonAverageDamage output.WithPoisonAverageDamage = baseDPS + output.TotalPoisonAverageDamage else output.CombinedDPS = output.CombinedDPS + output.TotalPoisonDPS output.WithPoisonDPS = baseDPS + output.TotalPoisonDPS end end if skillFlags.ignite then if skillFlags.igniteCanStack then if env.mode_average 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 -- Print various tables to the console local function infoDump(env, output) env.modDB:Print() ConPrintf("=== Enemy Mod DB ===") env.enemyDB:Print() ConPrintf("=== Main Skill ===") for _, gem in ipairs(env.mainSkill.gemList) do ConPrintf("%s %d/%d", gem.name, gem.level, gem.quality) end ConPrintf("=== Main Skill Flags ===") ConPrintf("Mod: %s", modLib.formatFlags(env.mainSkill.skillCfg.flags, ModFlag)) ConPrintf("Keyword: %s", modLib.formatFlags(env.mainSkill.skillCfg.keywordFlags, KeywordFlag)) ConPrintf("=== Main Skill Mods ===") env.mainSkill.skillModList:Print() ConPrintf("== Aux Skills ==") for i, aux in ipairs(env.auxSkillList) do ConPrintf("Skill #%d:", i) for _, gem in ipairs(aux.gemList) do ConPrintf(" %s %d/%d", gem.name, gem.level, gem.quality) end end -- ConPrintf("== Conversion Table ==") -- ConPrintTable(env.conversionTable) ConPrintf("== Output Table ==") local outNames = { } for name in pairs(env.output) do t_insert(outNames, name) end table.sort(outNames) for _, name in ipairs(outNames) do if type(env.output[name]) == "table" then local subNames = { } for subName in pairs(env.output[name]) do t_insert(subNames, subName) end table.sort(subNames) for _, subName in ipairs(subNames) do ConPrintf("%s.%s = %s", name, subName, tostring(env.output[name][subName])) end else ConPrintf("%s = %s", name, tostring(env.output[name])) end end end -- Generate a function for calculating the effect of some modification to the environment local function getCalculator(build, fullInit, modFunc) -- Initialise environment local env = initEnv(build, "CALCULATOR") -- Save a copy of the initial mod database local initModDB = common.New("ModDB") initModDB:AddDB(env.modDB) initModDB.conditions = copyTable(env.modDB.conditions) initModDB.multipliers = copyTable(env.modDB.multipliers) local initEnemyDB = common.New("ModDB") initEnemyDB:AddDB(env.enemyDB) initEnemyDB.conditions = copyTable(env.enemyDB.conditions) initEnemyDB.multipliers = copyTable(env.enemyDB.multipliers) -- Run base calculation pass performCalcs(env) local baseOutput = env.output return function(...) -- Restore initial mod database env.modDB.mods = wipeTable(env.modDB.mods) env.modDB:AddDB(initModDB) env.modDB.conditions = copyTable(initModDB.conditions) env.modDB.multipliers = copyTable(initModDB.multipliers) env.enemyDB.mods = wipeTable(env.enemyDB.mods) env.enemyDB:AddDB(initEnemyDB) env.enemyDB.conditions = copyTable(initEnemyDB.conditions) env.enemyDB.multipliers = copyTable(initEnemyDB.multipliers) -- Call function to make modifications to the enviroment modFunc(env, ...) -- Run calculation pass performCalcs(env) return env.output end, baseOutput end local calcs = { } -- Get fast calculator for adding tree node modifiers function calcs.getNodeCalculator(build) return getCalculator(build, true, function(env, nodeList) -- Build and merge modifiers for these nodes env.modDB:AddList(buildNodeModList(env, nodeList)) --[[local nodeModList = buildNodeModList(env, nodeList) if remove then for _, mod in ipairs(nodeModList) do if mod.type == "LIST" or mod.type == "FLAG" then for i, dbMod in ipairs(env.modDB.mods[mod.name] or { }) do if mod == dbMod then t_remove(env.modDB.mods[mod.name], i) break end end elseif mod.type == "MORE" then env.modDB:NewMod(mod.name, mod.type, (1 / (1 + mod.value / 100) - 1) * 100, mod.source, mod.flags, mod.keywordFlags, unpack(mod.tagList)) else env.modDB:NewMod(mod.name, mod.type, -mod.value, mod.source, mod.flags, mod.keywordFlags, unpack(mod.tagList)) end end else env.modDB:AddList(nodeModList) end]] end) end -- Get calculator for other changes (adding/removing nodes, items, gems, etc) function calcs.getMiscCalculator(build) -- Run base calculation pass local env = initEnv(build, "CALCULATOR") performCalcs(env) local baseOutput = env.output return function(override) env = initEnv(build, "CALCULATOR", override) performCalcs(env) return env.output end, baseOutput end -- Build output for display in the side bar or calcs tab function calcs.buildOutput(build, mode) -- Build output local env = initEnv(build, mode) performCalcs(env) local output = env.output if mode == "MAIN" then output.ExtraPoints = env.modDB:Sum("BASE", nil, "ExtraPoints") local specCfg = { source = "Tree" } for _, stat in pairs({"Life", "Mana", "Armour", "Evasion", "EnergyShield"}) do output["Spec:"..stat.."Inc"] = env.modDB:Sum("INC", specCfg, stat) end env.conditionsUsed = { } local function addCond(var, mod) if not env.conditionsUsed[var] then env.conditionsUsed[var] = { } end t_insert(env.conditionsUsed[var], mod) end for _, db in ipairs{env.modDB, env.enemyDB} do for modName, modList in pairs(db.mods) do for _, mod in ipairs(modList) do for _, tag in ipairs(mod.tagList) do if tag.type == "Condition" then if tag.varList then for _, var in ipairs(tag.varList) do addCond(var, mod) end else addCond(tag.var, mod) end end end end end end elseif mode == "CALCS" then local buffList = { } local combatList = { } local curseList = { } if output.PowerCharges > 0 then t_insert(combatList, s_format("%d Power Charges", output.PowerCharges)) end if output.FrenzyCharges > 0 then t_insert(combatList, s_format("%d Frenzy Charges", output.FrenzyCharges)) end if output.EnduranceCharges > 0 then t_insert(combatList, s_format("%d Endurance Charges", output.EnduranceCharges)) end if env.modDB.conditions.Onslaught then t_insert(combatList, "Onslaught") end if env.modDB.conditions.UnholyMight then t_insert(combatList, "Unholy Might") end for _, activeSkill in ipairs(env.activeSkillList) do if activeSkill.buffSkill then if activeSkill.skillFlags.multiPart then t_insert(buffList, activeSkill.activeGem.name .. " (" .. activeSkill.skillPartName .. ")") else t_insert(buffList, activeSkill.activeGem.name) end end if activeSkill.debuffSkill then if activeSkill.skillFlags.multiPart then t_insert(curseList, activeSkill.activeGem.name .. " (" .. activeSkill.skillPartName .. ")") else t_insert(curseList, activeSkill.activeGem.name) end end end for _, value in ipairs(env.modDB:Sum("LIST", nil, "ExtraCurse")) do t_insert(curseList, value.name) end output.BuffList = table.concat(buffList, ", ") output.CombatList = table.concat(combatList, ", ") output.CurseList = table.concat(curseList, ", ") infoDump(env) end return env end return calcs