-- Path of Building -- -- Module: Calcs -- Performs all the offense and defense calculations. -- Here be dragons! -- local grid = ... local pairs = pairs local ipairs = ipairs local t_insert = table.insert 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 mod_listMerge = modLib.listMerge local mod_listScaleMerge = modLib.listScaleMerge local mod_dbMerge = modLib.dbMerge local mod_dbScaleMerge = modLib.dbScaleMerge local mod_dbUnmerge = modLib.dbUnmerge local mod_dbMergeList = modLib.dbMergeList local mod_dbScaleMergeList = modLib.dbScaleMergeList local mod_dbUnmergeList = modLib.dbUnmergeList local setViewMode = LoadModule("Modules/CalcsView", grid) local isElemental = { fire = true, cold = true, lightning = true } -- List of all damage types, ordered according to the conversion sequence local dmgTypeList = {"physical", "lightning", "cold", "fire", "chaos"} -- Combine specified modifiers from all current namespaces local function sumMods(modDB, mult, ...) local activeWatchers = modDB._activeWatchers local val = mult and 1 or 0 for i = 1, select('#', ...) do local modName = select(i, ...) if modName then for space, spaceName in pairs(modDB._spaces) do local modVal = space[modName] if modVal then val = mult and (val * modVal) or (val + modVal) end if activeWatchers then local fullName = (spaceName and spaceName.."_" or "") .. modName for watchList in pairs(activeWatchers) do watchList[fullName] = mult and 1 or 0 end end end end end return val end -- Get value of misc modifier local function getMiscVal(modDB, spaceName, modName, default) local space = modDB[spaceName or "global"] local val = default if space and space[modName] ~= nil then val = space[modName] end if modDB._activeWatchers then local fullName = (spaceName and spaceName.."_" or "") .. modName for watchList in pairs(modDB._activeWatchers) do watchList[fullName] = default end if not space then modDB[spaceName] = { } end end return val end -- Calculate value, optionally adding additional base local function calcVal(modDB, name, base) local baseVal = sumMods(modDB, false, name.."Base") + (base or 0) return baseVal * (1 + (sumMods(modDB, false, name.."Inc")) / 100) * sumMods(modDB, true, name.."More") 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) / 100, 0.95), 0.05) end -- Merge gem modifiers with given mod list local function mergeGemMods(modList, gem) for k, v in pairs(gem.data.base) do mod_listMerge(modList, k, v) end for k, v in pairs(gem.data.quality) do mod_listMerge(modList, k, m_floor(v * gem.effectiveQuality)) end for k, v in pairs(gem.data.levels[gem.effectiveLevel]) do mod_listMerge(modList, k, v) end end -- Generate active namespace table local function buildSpaceTable(modDB, spaceFlags) modDB._spaces = { [modDB.global] = false } if spaceFlags then for spaceName, val in pairs(spaceFlags) do if val then modDB[spaceName] = modDB[spaceName] or { } if modDB._activeWatchers or next(modDB[spaceName]) then modDB._spaces[modDB[spaceName]] = spaceName end end end end end -- Start watch section local function startWatch(env, key, ...) if env.buildWatch then -- Running in build mode -- In this mode, any modifiers read while this section is active will be tracked env.watchers[key] = { _key = key } env.modDB._activeWatchers[env.watchers[key] ] = true return true else local watchers = env.watchers if not watchers or env.spacesChanged then -- Watch system is disabled or skill namespaces have changed, so all sections will run by default return true end -- This section will be flagged if any modifiers read during build mode have changed since the build mode pass if not watchers[key] or watchers[key]._flag then return true end for i = 1, select('#', ...) do -- Check if any dependant sections have been flagged if watchers[select(i, ...)]._flag then return true end end return false end end -- End watch section local function endWatch(env, key) if env.buildWatch and env.watchers[key] then env.modDB._activeWatchers[env.watchers[key] ] = nil end end -- Performs some preliminary processing of the given skill -- It will find the active skill gem, determines the base flag set, and check that the support gems can support this skill local function validateSkillSupports(skill) -- Build gem list local gemList = { } for _, gem in pairs(skill.gemList) do if gem.name then t_insert(gemList, gem) end end -- Find active skill skill.activeGem = nil for _, gem in ipairs(gemList) do if not gem.data.support then skill.activeGem = gem break end end -- Default attack if no active gem provided if not skill.activeGem then skill.activeGem = { name = "Default Attack", level = 1, quality = 0, data = data.gems._default } gemList = { skill.activeGem } end -- Build base skill flag set ('attack', 'projectile', etc) local baseFlags = { } skill.baseFlags = baseFlags for k, v in pairs(skill.activeGem.data) do if v == true then baseFlags[k] = true end end for _, gem in ipairs(gemList) do if gem.data.support and gem.data.addFlags then -- Support gem adds flags to supported skills (eg. Remote Mine adds 'mine') for k in pairs(gem.data.addFlags) do baseFlags[k] = true end end end -- Process support gems skill.validGemList = { } for _, gem in ipairs(gemList) do if gem.data.support and ( (gem.data.attack and not baseFlags.attack) or (gem.data.spell and not baseFlags.spell) or (gem.data.melee and not baseFlags.melee) or (gem.data.projectile and not baseFlags.projectile) or (gem.data.chaining and not (baseFlags.chaining or baseFlags.projectile) and not (gem.data.projectile and baseFlags.projectile)) or (gem.data.duration and not baseFlags.duration) or (gem.data.totem and not baseFlags.totem) or (gem.data.trap and not baseFlags.trap and not (gem.data.mine and baseFlags.mine)) or (gem.data.mine and not baseFlags.mine and not (gem.data.trap and baseFlags.trap)) ) then -- This support doesn't apply gem.calcsErrMsg = skill.activeGem.name.." cannot be supported by "..gem.name elseif not gem.data.support and gem ~= skill.activeGem then gem.calcsErrMsg = "You can only specify one active gem per skill, so this one will be ignored" else gem.calcsErrMsg = nil t_insert(skill.validGemList, gem) end end end -- Build list of modifiers for given skill local function buildSkillModList(env, skill) -- Initialise effective gem level and quality for _, gem in pairs(skill.validGemList) do gem.effectiveLevel = gem.level gem.effectiveQuality = gem.quality end local skillModList = { } skill.skillModList = skillModList if skill.slot then -- Check for local skill modifiers from the item this skill is socketed into local slotSpace = env.modDB["SocketedIn:"..skill.slot] if slotSpace then for k, v in pairs(slotSpace) do local factor, type = k:match("gem(%a+)_(%a+)") if factor then -- Gem level/quality modifier, apply it now for _, gem in pairs(skill.validGemList) do if type == "all" or (type == "active" and not gem.data.support) or gem.data[type] then -- This modifier applies to this type of gem if factor == "Level" then gem.effectiveLevel = gem.effectiveLevel + v elseif factor == "Quality" then gem.effectiveQuality = gem.effectiveQuality + v end end end else -- Merge with the skill modifier list mod_listMerge(skillModList, k, v) end end end end -- Merge skill-specific modifiers local skillSpace = env.modDB["Skill:"..skill.activeGem.name] if skillSpace then for k, v in pairs(skillSpace) do mod_listMerge(skillModList, k, v) end end -- Add support gem modifiers to skill mod list for _, gem in pairs(skill.validGemList) do if gem.data.support then mergeGemMods(skillModList, gem) end end -- Apply gem/quality modifiers from support gems skill.activeGem.effectiveLevel = skill.activeGem.effectiveLevel + (skillModList.gemLevel_active or 0) skill.activeGem.effectiveQuality = skill.activeGem.effectiveQuality + (skillModList.gemQuality_active or 0) -- Add active gem modifiers mergeGemMods(skillModList, skill.activeGem) -- Separate auxillary modifiers (mods that can affect defensive stats or other skills) skill.buffModList = { } skill.auraModList = { } skill.curseModList = { } for k, v in pairs(skillModList) do local spaceName, modName = modLib.getSpaceName(k) if spaceName == "BuffEffect" then mod_listMerge(skill.buffModList, modName, v) elseif spaceName == "AuraEffect" then mod_listMerge(skill.auraModList, modName, v) elseif spaceName == "CurseEffect" then mod_listMerge(skill.curseModList, modName, v) end end if skill ~= env.mainSkill then -- Add to auxillary skill list t_insert(env.auxSkills, skill) 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 = { } for _, node in pairs(nodeList) do -- Merge with output list for k, v in pairs(node.modList) do mod_listMerge(modList, k, v) end -- Run radius jewels for _, rad in pairs(env.radiusJewelList) do local vX, vY = node.x - rad.x, node.y - rad.y if vX * vX + vY * vY <= rad.rSq 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) end end return modList end -- Calculate min/max damage of a hit for the given damage type local function calcHitDamage(env, output, damageType, ...) local modDB = env.modDB local isAttack = (env.mode_skillType == "ATTACK") local damageTypeMin = damageType.."Min" local damageTypeMax = damageType.."Max" -- Calculate base values local baseMin, baseMax if isAttack then baseMin = getMiscVal(modDB, "weapon1", damageTypeMin, 0) + sumMods(modDB, false, damageTypeMin) baseMax = getMiscVal(modDB, "weapon1", damageTypeMax, 0) + sumMods(modDB, false, damageTypeMax) else local damageEffectiveness = getMiscVal(modDB, "skill", "damageEffectiveness", 0) if damageEffectiveness == 0 then damageEffectiveness = 1 end baseMin = getMiscVal(modDB, "skill", damageTypeMin, 0) + sumMods(modDB, false, damageTypeMin) * damageEffectiveness baseMax = getMiscVal(modDB, "skill", damageTypeMax, 0) + sumMods(modDB, false, damageTypeMax) * damageEffectiveness end -- Build lists of applicable modifier names local addElemental = isElemental[damageType] local inc = { damageType.."Inc", "damageInc" } local more = { damageType.."More", "damageMore" } local damageTypeStr = "total_"..damageType for i = 1, select('#', ...) do local dstElem = select(i, ...) damageTypeStr = damageTypeStr..dstElem -- Add modifiers for damage types to which this damage is being converted addElemental = addElemental or isElemental[dstElem] t_insert(inc, dstElem.."Inc") t_insert(more, dstElem.."More") end if addElemental then -- Damage is elemental or is being converted to elemental damage, add global elemental modifiers t_insert(inc, "elementalInc") t_insert(more, "elementalMore") end -- Combine modifiers local damageTypeStrInc = damageTypeStr.."Inc" if startWatch(env, damageTypeStrInc) then output[damageTypeStrInc] = sumMods(modDB, false, unpack(inc)) endWatch(env, damageTypeStrInc) end local damageTypeStrMore = damageTypeStr.."More" if startWatch(env, damageTypeStrMore) then output[damageTypeStrMore] = sumMods(modDB, true, unpack(more)) endWatch(env, damageTypeStrMore) end local modMult = (1 + output[damageTypeStrInc] / 100) * output[damageTypeStrMore] -- Calculate conversions if startWatch(env, damageTypeStr.."Conv", "conversionTable") then local addMin, addMax = 0, 0 local conversionTable = output.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, output, otherType, damageType, ...) addMin = addMin + min * convMult addMax = addMax + max * convMult end end output[damageTypeStr.."ConvAddMin"] = addMin output[damageTypeStr.."ConvAddMax"] = addMax endWatch(env, damageTypeStr.."Conv") end return (baseMin * modMult + output[damageTypeStr.."ConvAddMin"]), (baseMax * modMult + output[damageTypeStr.."ConvAddMax"]) 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. Merge main modifiers (mergeMainMods) -- 3. Finalise modifier database (finaliseMods) -- 4. Run calculations (performCalcs) -- -- Thus a basic calculation pass would look like this: -- -- local env = initEnv(input, build) -- mergeMainMods(env) -- finaliseMods(env, output) -- performCalcs(env, output) -- -- Initialise environment -- This will initialise the skill list and the modifier database local function initEnv(build, input, mode) local env = { } env.build = build -- Make a local copy of the skill list env.skills = { } for _, skill in pairs(build.skillsTab.list) do t_insert(env.skills, skill) end if #env.skills == 0 then -- No skills found, so add dummy skill to stop everything exploding t_insert(env.skills, { gemList = { } }) end -- Process the skills for _, skill in pairs(env.skills) do validateSkillSupports(skill) end -- Select main skill if mode == "GRID" then input.skill_number = m_min(#env.skills, input.skill_number or 1) env.mainSkillIndex = input.skill_number env.skillPart = input.skill_part or 1 env.buffMode = input.misc_buffMode else build.mainSkillIndex = m_min(#env.skills, build.mainSkillIndex or 1) env.mainSkillIndex = build.mainSkillIndex env.skillPart = env.skills[env.mainSkillIndex].skillPart or 1 env.buffMode = "EFFECTIVE" end env.mainSkill = env.skills[env.mainSkillIndex] env.setupFunc = env.mainSkill.activeGem.data.setupFunc -- Handle multipart skills env.skillParts = { } local activeGemParts = env.mainSkill.activeGem.data.parts if activeGemParts then for i, part in pairs(activeGemParts) do env.skillParts[i] = part.name or "" end env.skillPart = m_min(#activeGemParts, env.skillPart) local part = activeGemParts[env.skillPart] for k, v in pairs(part) do if v == true then env.mainSkill.baseFlags[k] = true elseif v == false then env.mainSkill.baseFlags[k] = nil end end env.mainSkill.baseFlags.multiPart = #activeGemParts > 1 end -- Initialise modifier database with base values local modDB = { } env.modDB = modDB env.classId = build.spec.curClassId local classStats = build.tree.characterData[env.classId] for _, stat in pairs({"str","dex","int"}) do mod_dbMerge(modDB, "", stat.."Base", classStats["base_"..stat]) end local level = build.characterLevel mod_dbMerge(modDB, "", "lifeBase", 38 + level * 12) mod_dbMerge(modDB, "", "manaBase", 34 + level * 6) mod_dbMerge(modDB, "", "evasionBase", 53 + level * 3) mod_dbMerge(modDB, "", "accuracyBase", (level - 1) * 2) mod_dbMerge(modDB, "", "fireResistMax", 75) mod_dbMerge(modDB, "", "coldResistMax", 75) mod_dbMerge(modDB, "", "lightningResistMax", 75) mod_dbMerge(modDB, "", "chaosResistMax", 75) mod_dbMerge(modDB, "", "blockChanceMax", 75) mod_dbMerge(modDB, "", "powerMax", 3) mod_dbMerge(modDB, "PerPower", "critChanceInc", 50) mod_dbMerge(modDB, "", "frenzyMax", 3) mod_dbMerge(modDB, "PerFrenzy", "speedInc", 4) mod_dbMerge(modDB, "PerFrenzy", "damageMore", 1.04) mod_dbMerge(modDB, "", "enduranceMax", 3) mod_dbMerge(modDB, "PerEndurance", "elementalResist", 4) mod_dbMerge(modDB, "", "activeTrapLimit", 3) mod_dbMerge(modDB, "", "activeMineLimit", 5) mod_dbMerge(modDB, "", "projectileCount", 1) -- Add bandit mods if build.banditNormal == "Alira" then mod_dbMerge(modDB, "", "manaBase", 60) elseif build.banditNormal == "Kraityn" then mod_dbMerge(modDB, "", "elementalResist", 10) elseif build.banditNormal == "Oak" then mod_dbMerge(modDB, "", "lifeBase", 40) else mod_dbMerge(modDB, "", "extraPoints", 1) end if build.banditCruel == "Alira" then mod_dbMerge(modDB, "", "castSpeedInc", 5) elseif build.banditCruel == "Kraityn" then mod_dbMerge(modDB, "", "attackSpeedInc", 8) elseif build.banditCruel == "Oak" then mod_dbMerge(modDB, "", "physicalInc", 16) else mod_dbMerge(modDB, "", "extraPoints", 1) end if build.banditMerciless == "Alira" then mod_dbMerge(modDB, "", "powerMax", 1) elseif build.banditMerciless == "Kraityn" then mod_dbMerge(modDB, "", "frenzyMax", 1) elseif build.banditMerciless == "Oak" then mod_dbMerge(modDB, "", "enduranceMax", 1) else mod_dbMerge(modDB, "", "extraPoints", 1) end -- Add mods from the input table mod_dbMergeList(modDB, input) return env end -- This function: -- 1. Merges modifiers for all items, optionally replacing one item -- 2. Builds a list of jewels with radius functions -- 3. Builds modifier lists for all active skills -- 4. Merges modifiers for all allocated passive nodes local function mergeMainMods(env, repSlotName, repItem) local build = env.build -- Build and merge item modifiers, and create list of radius jewels env.itemModList = wipeTable(env.itemModList) env.radiusJewelList = wipeTable(env.radiusJewelList) for slotName, slot in pairs(build.itemsTab.slots) do local item if slotName == repSlotName then item = repItem else item = build.itemsTab.list[slot.selItemId] end if slot.nodeId then -- Slot is a jewel socket, check if socket is allocated if not build.spec.allocNodes[slot.nodeId] then item = nil elseif item and item.jewelRadiusIndex and item.jewelFunc then -- Jewel has a defined radius function, add it to the list local radiusInfo = data.jewelRadius[item.jewelRadiusIndex] local node = build.spec.nodes[slot.nodeId] t_insert(env.radiusJewelList, { rSq = radiusInfo.rad * radiusInfo.rad, x = node.x, y = node.y, func = item.jewelFunc, data = { } }) end end if item then -- Merge mods for this item into the global item mod list local srcList = item.modList or item.slotModList[slot.slotNum] for k, v in pairs(srcList) do mod_listMerge(env.itemModList, k, v) end if item.type ~= "Jewel" then -- Update item counts if item.rarity == "UNIQUE" then mod_listMerge(env.itemModList, "gear_UniqueCount", 1) elseif item.rarity == "RARE" then mod_listMerge(env.itemModList, "gear_RareCount", 1) elseif item.rarity == "MAGIC" then mod_listMerge(env.itemModList, "gear_MagicCount", 1) else mod_listMerge(env.itemModList, "gear_NormalCount", 1) end end end end mod_dbMergeList(env.modDB, env.itemModList) -- Build skill modifier lists env.auxSkills = { } for _, skill in pairs(env.skills) do if skill == env.mainSkill or skill.active then buildSkillModList(env, skill) end end -- Build and merge modifiers for allocated passives env.specModList = buildNodeModList(env, build.spec.allocNodes, true) mod_dbMergeList(env.modDB, env.specModList) end -- Prepare environment for calculations local function finaliseMods(env, output) local modDB = env.modDB local weapon1Type = getMiscVal(modDB, "weapon1", "type", "None") local weapon2Type = getMiscVal(modDB, "weapon2", "type", "") if weapon1Type == output.weapon1Type and weapon2Type == output.weapon2Type then env.spacesChanged = false else env.spacesChanged = true output.weapon1Type = weapon1Type output.weapon2Type = weapon2Type -- Initialise skill flag set env.skillFlags = wipeTable(env.skillFlags) local skillFlags = env.skillFlags for k, v in pairs(env.mainSkill.baseFlags) do skillFlags[k] = v end -- Set weapon flags skillFlags.mainIs1H = true local weapon1Info = data.weaponTypeInfo[weapon1Type] if weapon1Info then if not weapon1Info.oneHand then skillFlags.mainIs1H = nil end if skillFlags.attack then skillFlags.weapon1Attack = true if weapon1Info.melee then skillFlags.bow = nil skillFlags.projectile = nil else skillFlags.melee = nil end end end local weapon2Info = data.weaponTypeInfo[weapon2Type] if weapon2Info and skillFlags.mainIs1H then if skillFlags.attack then skillFlags.weapon2Attack = true end end -- Build list of namespaces to search for mods local skillSpaceFlags = wipeTable(env.skillSpaceFlags) env.skillSpaceFlags = skillSpaceFlags if skillFlags.spell then skillSpaceFlags["spell"] = true elseif skillFlags.attack then skillSpaceFlags["attack"] = true end if skillFlags.weapon1Attack then if getMiscVal(modDB, "weapon1", "varunastra", false) then skillSpaceFlags["axe"] = true skillSpaceFlags["claw"] = true skillSpaceFlags["dagger"] = true skillSpaceFlags["mace"] = true skillSpaceFlags["sword"] = true else skillSpaceFlags[weapon1Info.space] = true end if weapon1Type ~= "None" then skillSpaceFlags["weapon"] = true if skillFlags.mainIs1H then skillSpaceFlags["weapon1h"] = true if weapon1Info.melee then skillSpaceFlags["weapon1hMelee"] = true else skillSpaceFlags["weaponRanged"] = true end else skillSpaceFlags["weapon2h"] = true if weapon1Info.melee then skillSpaceFlags["weapon2hMelee"] = true else skillSpaceFlags["weaponRanged"] = true end end end end if skillFlags.melee then skillSpaceFlags["melee"] = true elseif skillFlags.projectile then skillSpaceFlags["projectile"] = true if skillFlags.attack then skillSpaceFlags["projectileAttack"] = true end end if skillFlags.totem then skillSpaceFlags["totem"] = true elseif skillFlags.trap then skillSpaceFlags["trap"] = true elseif skillFlags.mine then skillSpaceFlags["mine"] = true end if skillFlags.aoe then skillSpaceFlags["aoe"] = true end if skillFlags.debuff then skillSpaceFlags["debuff"] = true end if skillFlags.aura then skillSpaceFlags["aura"] = true end if skillFlags.curse then skillSpaceFlags["curse"] = true end if skillFlags.warcry then skillSpaceFlags["warcry"] = true end if skillFlags.movement then skillSpaceFlags["movementSkills"] = true end if skillFlags.lightning then skillSpaceFlags["lightningSkills"] = true skillSpaceFlags["elementalSkills"] = true end if skillFlags.cold then skillSpaceFlags["coldSkills"] = true skillSpaceFlags["elementalSkills"] = true end if skillFlags.fire then skillSpaceFlags["fireSkills"] = true skillSpaceFlags["elementalSkills"] = true end if skillFlags.chaos then skillSpaceFlags["chaosSkills"] = true end end if weapon1Type == "None" then -- Merge unarmed weapon modifiers for k, v in pairs(data.unarmedWeapon[env.classId]) do mod_dbMerge(modDB, "weapon1", k, v) end end -- Set modes if env.mainSkill.baseFlags.attack then env.mode_skillType = "ATTACK" else env.mode_skillType = "SPELL" end if env.skillFlags.showAverage then env.mode_average = true end local buffMode = env.buffMode if buffMode == "BUFFED" then env.mode_buffs = true env.mode_effective = false elseif buffMode == "EFFECTIVE" then env.mode_buffs = true env.mode_effective = true else env.mode_buffs = false env.mode_effective = false end -- Reset namespaces buildSpaceTable(modDB) -- Merge skill modifiers and calculate life and mana reservations output.lifeReservedBase = 0 output.lifeReservedPercent = 0 output.manaReservedBase = 0 output.manaReservedPercent = 0 for _, skill in pairs(env.skills) do if skill == env.mainSkill or skill.active then local skillModList = skill.skillModList -- Merge skill modifiers if skill == env.mainSkill then mod_dbMergeList(modDB, skillModList) end mod_dbMergeList(modDB, skill.buffModList) local auraEffect = (1 + (getMiscVal(modDB, nil, "auraEffectInc", 0) + (skillModList.auraEffectInc or 0)) / 100) * getMiscVal(modDB, nil, "auraEffectMore", 1) * (skillModList.auraEffectMore or 1) mod_dbScaleMergeList(modDB, skill.auraModList, auraEffect) if env.mode_effective then local curseEffect = (1 + (getMiscVal(modDB, nil, "curseEffectInc", 0) + (skillModList.curseEffectInc or 0)) / 100) * getMiscVal(modDB, nil, "curseEffectMore", 1) * (skillModList.curseEffectMore or 1) mod_dbScaleMergeList(modDB, skill.curseModList, curseEffect) end -- Calculate reservations local baseVal, suffix baseVal = skillModList.skill_manaReservedBase if baseVal then suffix = "Base" else baseVal = skillModList.skill_manaReservedPercent if baseVal then suffix = "Percent" end end if baseVal then local cost = m_floor(baseVal * (skillModList.manaCostMore or 1)) cost = m_ceil(cost * sumMods(modDB, true, "manaReservedMore") * (skillModList.manaReservedMore or 1)) cost = m_ceil(cost * (1 + sumMods(modDB, false, "manaReservedInc") / 100 + (skillModList.manaReservedInc or 0) / 100)) if getMiscVal(modDB, nil, "bloodMagic", false) or skillModList.skill_bloodMagic then output["lifeReserved"..suffix] = output["lifeReserved"..suffix] + cost else output["manaReserved"..suffix] = output["manaReserved"..suffix] + cost end end end end -- Merge active skill part mods if env.mainSkill.baseFlags.multiPart and modDB["SkillPart"..env.skillPart] then mod_dbMergeList(modDB, modDB["SkillPart"..env.skillPart]) end -- Merge gear-sourced keystone modifiers if modDB.gear then for name, node in pairs(env.build.tree.keystoneMap) do if getMiscVal(modDB, "gear", "keystone:"..name, false) and not getMiscVal(modDB, nil, "keystone:"..name, false) then -- Keystone is granted by gear but not allocated on tree, so add its modifiers mod_dbMergeList(modDB, buildNodeModList(env, { node })) end end end -- Build condition list local condList = wipeTable(env.condList) env.condList = condList if weapon1Type == "Staff" then condList["UsingStaff"] = true end if env.skillFlags.mainIs1H then if weapon2Type == "Shield" then condList["UsingShield"] = true end end if getMiscVal(modDB, "gear", "NormalCount", 0) > 0 then condList["UsingNormalItem"] = true end if getMiscVal(modDB, "gear", "MagicCount", 0) > 0 then condList["UsingMagicItem"] = true end if getMiscVal(modDB, "gear", "RareCount", 0) > 0 then condList["UsingRareItem"] = true end if getMiscVal(modDB, "gear", "UniqueCount", 0) > 0 then condList["UsingUniqueItem"] = true end if output.manaReservedBase == 0 and output.manaReservedPercent == 0 then condList["NoManaReserved"] = true end if modDB.Cond then for k, v in pairs(modDB.Cond) do condList[k] = v if v then env.skillFlags[k] = true end end end if env.mode_buffs then if modDB.CondBuff then for k, v in pairs(modDB.CondBuff) do condList[k] = v if v then env.skillFlags[k] = true end end end if modDB.CondEff and env.mode_effective then for k, v in pairs(modDB.CondEff) do condList[k] = v if v then env.skillFlags[k] = true end end mod_dbMerge(modDB, "CondMod", "EnemyShocked_effective_damageTakenInc", 50) condList["EnemyFrozenShockedIgnited"] = condList["EnemyFrozen"] or condList["EnemyShocked"] or condList["EnemyIgnited"] condList["EnemyElementalStatus"] = condList["EnemyChilled"] or condList["EnemyFrozen"] or condList["EnemyShocked"] or condList["EnemyIgnited"] end if not getMiscVal(modDB, nil, "neverCrit", false) then condList["CritInPast8Sec"] = true end if env.skillFlags.attack then condList["AttackedRecently"] = true elseif env.skillFlags.spell then condList["CastSpellRecently"] = true end if env.skillFlags.movement then condList["UsedMovementSkillRecently"] = true end if env.skillFlags.totem then condList["SummonedTotemRecently"] = true end if env.skillFlags.mine then condList["DetonatedMinesRecently"] = true end end -- Build and merge conditional modifier list local condModList = wipeTable(env.condModList) env.condModList = condModList if modDB.CondMod then for k, v in pairs(modDB.CondMod) do local isNot, condName, modName = modLib.getCondName(k) if (isNot and not condList[condName]) or (not isNot and condList[condName]) then mod_listMerge(condModList, modName, v) end end end mod_dbMergeList(modDB, env.condModList) -- Add boss modifiers if getMiscVal(modDB, "effective", "enemyIsBoss", false) then --mod_dbMerge(modDB, "", "curseEffectInc", -60) mod_dbMerge(modDB, "", "curseEffectMore", 0.4) -- FIXME: Need to confirm actual value mod_dbMerge(modDB, "effective", "elementalResist", 30) mod_dbMerge(modDB, "effective", "chaosResist", 15) end -- Add per-item-type mods for spaceName, countName in pairs({["PerNormal"]="NormalCount",["PerMagic"]="MagicCount",["PerRare"]="RareCount",["PerUnique"]="UniqueCount",["PerGrandSpectrum"]="GrandSpectrumCount"}) do local space = modDB[spaceName] if space then local count = getMiscVal(modDB, "gear", countName, 0) for k, v in pairs(space) do mod_dbScaleMerge(modDB, "", k, v, count) end end end -- Calculate maximum charges if getMiscVal(modDB, "buff", "power", false) then env.skillFlags.havePower = true output.powerMax = getMiscVal(modDB, nil, "powerMax", 0) end if getMiscVal(modDB, "buff", "frenzy", false) then env.skillFlags.haveFrenzy = true output.frenzyMax = getMiscVal(modDB, nil, "frenzyMax", 0) end if getMiscVal(modDB, "buff", "endurance", false) then env.skillFlags.haveEndurance = true output.enduranceMax = getMiscVal(modDB, nil, "enduranceMax", 0) end if env.mode_buffs then -- Build buff mod list local buffModList = wipeTable(env.buffModList) env.buffModList = buffModList -- Calculate total charge bonuses if env.skillFlags.havePower then for k, v in pairs(modDB.PerPower) do mod_listScaleMerge(buffModList, k, v, output.powerMax) end end if env.skillFlags.haveFrenzy then for k, v in pairs(modDB.PerFrenzy) do mod_listScaleMerge(buffModList, k, v, output.frenzyMax) end end if env.skillFlags.haveEndurance then for k, v in pairs(modDB.PerEndurance) do mod_listScaleMerge(buffModList, k, v, output.enduranceMax) end end -- Add other buffs if env.condList["Onslaught"] then local effect = m_floor(20 * (1 + sumMods(modDB, false, "onslaughtEffectInc") / 100)) mod_listMerge(buffModList, "attackSpeedInc", effect) mod_listMerge(buffModList, "castSpeedInc", effect) mod_listMerge(buffModList, "movementSpeedInc", effect) end if getMiscVal(modDB, "buff", "pendulum", false) then mod_listMerge(buffModList, "elementalInc", 100) mod_listMerge(buffModList, "aoeRadiusInc", 25) end -- Merge buff bonuses mod_dbMergeList(modDB, buffModList) end -- Calculate attributes for _, stat in pairs({"str","dex","int"}) do output["total_"..stat] = m_floor(calcVal(modDB, stat)) end -- Add attribute bonuses mod_dbMerge(modDB, "", "lifeBase", m_floor(output.total_str / 2)) local strDmgBonus = m_floor((output.total_str + getMiscVal(modDB, nil, "dexIntToMeleeBonus", 0)) / 5 + 0.5) mod_dbMerge(modDB, "melee", "physicalInc", strDmgBonus) if getMiscVal(modDB, nil, "ironGrip", false) then mod_dbMerge(modDB, "projectileAttack", "physicalInc", strDmgBonus) end if getMiscVal(modDB, nil, "ironWill", false) then mod_dbMerge(modDB, "spell", "damageInc", strDmgBonus) end mod_dbMerge(modDB, "", "accuracyBase", output.total_dex * 2) if not getMiscVal(modDB, nil, "ironReflexes", false) then mod_dbMerge(modDB, "", "evasionInc", m_floor(output.total_dex / 5 + 0.5)) end mod_dbMerge(modDB, "", "manaBase", m_ceil(output.total_int / 2)) mod_dbMerge(modDB, "", "energyShieldInc", m_floor(output.total_int / 5 + 0.5)) end -- Calculate offence and defence stats local function performCalcs(env, output) local modDB = env.modDB -- Calculate life/mana pools and defences if startWatch(env, "lifeES") then if getMiscVal(modDB, nil, "chaosInoculation", false) then output.total_life = 1 else output.total_life = calcVal(modDB, "life") end local convManaToES = getMiscVal(modDB, nil, "manaGainAsEnergyShield", 0) if convManaToES > 0 then output.total_energyShield = sumMods(modDB, false, "manaBase") * (1 + sumMods(modDB, false, "energyShieldInc", "defencesInc", "manaInc") / 100) * sumMods(modDB, true, "energyShieldMore", "defencesMore", "manaMore") * convManaToES / 100 else output.total_energyShield = 0 end output.total_gear_energyShieldBase = env.itemModList.energyShieldBase or 0 for _, slot in pairs({"global","slot:Helmet","slot:Body Armour","slot:Gloves","slot:Boots","slot:Shield"}) do buildSpaceTable(modDB, { [slot] = true }) local energyShieldBase = getMiscVal(modDB, slot, "energyShieldBase", 0) if energyShieldBase > 0 then output.total_energyShield = output.total_energyShield + energyShieldBase * (1 + sumMods(modDB, false, "energyShieldInc", "defencesInc") / 100) * sumMods(modDB, true, "energyShieldMore", "defencesMore") end if slot ~= "global" then output.total_gear_energyShieldBase = output.total_gear_energyShieldBase + energyShieldBase end end buildSpaceTable(modDB) output.total_energyShieldRecharge = output.total_energyShield * 0.2 * (1 + sumMods(modDB, false, "energyShieldRechargeInc", "energyShieldRecoveryInc") / 100) * sumMods(modDB, true, "energyShieldRechargeMore", "energyShieldRecoveryMore") output.total_energyShieldRechargeDelay = 2 / (1 + getMiscVal(modDB, nil, "energyShieldRechargeFaster", 0) / 100) if getMiscVal(modDB, nil, "noLifeRegen", false) then output.total_lifeRegen = 0 elseif getMiscVal(modDB, nil, "zealotsOath", false) then output.total_lifeRegen = 0 mod_dbMerge(modDB, "", "energyShieldRegenBase", sumMods(modDB, false, "lifeRegenBase")) mod_dbMerge(modDB, "", "energyShieldRegenPercent", sumMods(modDB, false, "lifeRegenPercent")) else mod_dbMerge(modDB, "", "lifeRegenBase", output.total_life * sumMods(modDB, false, "lifeRegenPercent") / 100) output.total_lifeRegen = sumMods(modDB, false, "lifeRegenBase") * (1 + sumMods(modDB, false, "lifeRecoveryInc") / 100) * sumMods(modDB, true, "lifeRecoveryMore") end mod_dbMerge(modDB, "", "energyShieldRegenBase", output.total_energyShield * sumMods(modDB, false, "energyShieldRegenPercent") / 100) output.total_energyShieldRegen = sumMods(modDB, false, "energyShieldRegenBase") * (1 + sumMods(modDB, false, "energyShieldRecoveryInc") / 100) * sumMods(modDB, true, "energyShieldRecoveryMore") endWatch(env, "lifeES") end if startWatch(env, "mana") then output.total_mana = calcVal(modDB, "mana") mod_dbMerge(modDB, "", "manaRegenBase", output.total_mana * 0.0175) output.total_manaRegen = sumMods(modDB, false, "manaRegenBase") * (1 + sumMods(modDB, false, "manaRegenInc", "manaRecoveryInc") / 100) * sumMods(modDB, true, "manaRegenMore", "manaRecoveryMore") endWatch(env, "mana") end if startWatch(env, "armourEvasion") then output.total_evasion = 0 output.total_armour = 0 output.total_gear_evasionBase = env.itemModList.evasionBase or 0 output.total_gear_armourBase = env.itemModList.armourBase or 0 local ironReflexes = getMiscVal(modDB, nil, "ironReflexes", false) if getMiscVal(modDB, nil, "unbreakable", false) then mod_dbMerge(modDB, "slot:Body Armour", "armourBase", getMiscVal(modDB, "slot:Body Armour", "armourBase", 0)) end for _, slot in pairs({"global","slot:Helmet","slot:Body Armour","slot:Gloves","slot:Boots","slot:Shield"}) do buildSpaceTable(modDB, { [slot] = true }) local evasionBase = getMiscVal(modDB, slot, "evasionBase", 0) local bothBase = getMiscVal(modDB, slot, "armourAndEvasionBase", 0) local armourBase = getMiscVal(modDB, slot, "armourBase", 0) if ironReflexes then if evasionBase > 0 or armourBase > 0 then output.total_armour = output.total_armour + (evasionBase + armourBase + bothBase) * (1 + sumMods(modDB, false, "armourInc", "evasionInc", "armourAndEvasionInc", "defencesInc") / 100) * sumMods(modDB, true, "armourMore", "evasionMore", "defencesMore") end else if evasionBase > 0 then output.total_evasion = output.total_evasion + (evasionBase + bothBase) * (1 + sumMods(modDB, false, "evasionInc", "armourAndEvasionInc", "defencesInc") / 100) * sumMods(modDB, true, "evasionMore", "defencesMore") end if armourBase > 0 then output.total_armour = output.total_armour + (armourBase + bothBase) * (1 + sumMods(modDB, false, "armourInc", "armourAndEvasionInc", "defencesInc") / 100) * sumMods(modDB, true, "armourMore", "defencesMore") end end if slot ~= "global" then output.total_gear_evasionBase = output.total_gear_evasionBase + evasionBase + bothBase output.total_gear_armourBase = output.total_gear_armourBase + armourBase + bothBase end end if getMiscVal(modDB, nil, "cannotEvade", false) then output.total_evadeChance = 0 else local attackerLevel = getMiscVal(modDB, "misc", "evadeMonsterLevel", false) and m_min(getMiscVal(modDB, "monster", "level", 1), #data.enemyAccuracyTable) or m_min(env.build.characterLevel, 80) output.total_evadeChance = 1 - calcHitChance(output.total_evasion, data.enemyAccuracyTable[attackerLevel]) end buildSpaceTable(modDB) endWatch(env, "armourEvasion") end if startWatch(env, "resist") then for _, elem in pairs({"fire", "cold", "lightning"}) do output["total_"..elem.."ResistMax"] = sumMods(modDB, false, elem.."ResistMax") output["total_"..elem.."Resist"] = m_min(sumMods(modDB, false, elem.."Resist", "elementalResist") - 60, output["total_"..elem.."ResistMax"]) end if getMiscVal(modDB, nil, "chaosInoculation", false) then output.total_chaosResistMax = 100 output.total_chaosResist = 100 else output.total_chaosResistMax = sumMods(modDB, false, "chaosResistMax") output.total_chaosResist = sumMods(modDB, false, "chaosResist") - 60 end endWatch(env, "resist") end if startWatch(env, "otherDef") then output.total_blockChanceMax = sumMods(modDB, false, "blockChanceMax") output.total_blockChance = m_min(sumMods(modDB, false, "blockChance") / 100 * (1 + sumMods(modDB, false, "blockChanceInc") / 100) * sumMods(modDB, true, "blockChanceMore"), output.total_blockChanceMax) output.total_spellBlockChance = m_min(sumMods(modDB, false, "spellBlockChance") / 100 * (1 + sumMods(modDB, false, "spellBlockChanceInc") / 100) * sumMods(modDB, true, "spellBlockChanceMore") + output.total_blockChance * m_min(100, getMiscVal(modDB, nil, "blockChanceConv", 0)) / 100, output.total_blockChanceMax) if getMiscVal(modDB, nil, "cannotBlockAttacks", false) then output.total_blockChance = 0 end output.total_dodgeAttacks = sumMods(modDB, false, "dodgeAttacks") / 100 output.total_dodgeSpells = sumMods(modDB, false, "dodgeSpells") / 100 local stunChance = 1 - sumMods(modDB, false, "avoidStun", 0) / 100 if output.total_energyShield > output.total_life * 2 then stunChance = stunChance * 0.5 end output.stun_avoidChance = 1 - stunChance if output.stun_avoidChance >= 1 then output.stun_duration = 0 output.stun_blockDuration = 0 else output.stun_duration = 0.35 / (1 + sumMods(modDB, false, "stunRecoveryInc") / 100) output.stun_blockDuration = 0.35 / (1 + sumMods(modDB, false, "stunRecoveryInc", "blockRecoveryInc") / 100) end endWatch(env, "otherDef") end -- Calculate life/mana reservation for _, pool in pairs({"life", "mana"}) do local max = output["total_"..pool] local reserved = output[pool.."ReservedBase"] + m_floor(max * output[pool.."ReservedPercent"] / 100 + 0.5) output["total_"..pool.."Reserved"] = reserved output["total_"..pool.."ReservedPercent"] = reserved / max output["total_"..pool.."Unreserved"] = max - reserved output["total_"..pool.."UnreservedPercent"] = (max - reserved) / max end -- Enable skill namespaces buildSpaceTable(modDB, env.skillSpaceFlags) -- Calculate projectile stats if env.skillFlags.projectile then if startWatch(env, "projectile") then output.total_projectileCount = sumMods(modDB, false, "projectileCount") output.total_pierce = m_min(100, sumMods(modDB, false, "pierceChance")) / 100 output.total_projectileSpeedMod = (1 + sumMods(modDB, false, "projectileSpeedInc") / 100) * sumMods(modDB, true, "projectileSpeedMore") endWatch(env, "projectile") end if getMiscVal(modDB, nil, "drillneck", false) then mod_dbMerge(modDB, "projectile", "damageInc", output.total_pierce * 100) end end -- Run skill setup function if env.setupFunc then env.setupFunc(function(mod, val) mod_dbMerge(modDB, nil, mod, val) end, output) end local isAttack = (env.mode_skillType == "ATTACK") -- Calculate enemy resistances if startWatch(env, "enemyResist") then local elemResist = getMiscVal(modDB, "effective", "elementalResist", 0) for _, damageType in pairs({"lightning","cold","fire"}) do output["enemy_"..damageType.."Resist"] = m_min(elemResist + getMiscVal(modDB, "effective", damageType.."Resist", 0), 75) end output.enemy_chaosResist = m_min(getMiscVal(modDB, "effective", "chaosResist", 0), 75) endWatch(env, "enemyResist") end -- Cache global damage disabling flags if startWatch(env, "canDeal") then output.canDeal = { } for _, damageType in pairs(dmgTypeList) do output.canDeal[damageType] = not getMiscVal(modDB, nil, "dealNo"..damageType, false) end endWatch(env, "canDeal") end local canDeal = output.canDeal -- Calculate damage conversion percentages if startWatch(env, "conversionTable") then output.conversionTable = { } for damageTypeIndex = 1, 4 do local damageType = dmgTypeList[damageTypeIndex] local globalConv = { } local skillConv = { } local add = { } 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] = sumMods(modDB, false, damageType.."ConvertTo"..otherType) globalTotal = globalTotal + globalConv[otherType] skillConv[otherType] = getMiscVal(modDB, "skill", damageType.."ConvertTo"..otherType, 0) skillTotal = skillTotal + skillConv[otherType] add[otherType] = sumMods(modDB, false, damageType.."GainAs"..otherType) end if 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] = globalConv[type] * 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 - (globalTotal + skillTotal) / 100 output.conversionTable[damageType] = dmgTable end output.conversionTable["chaos"] = { mult = 1 } endWatch(env, "conversionTable") end -- Calculate damage for each damage type local combMin, combMax = 0, 0 for _, damageType in pairs(dmgTypeList) do local min, max if startWatch(env, damageType, "enemyResist", "canDeal", "conversionTable") then if canDeal[damageType] then min, max = calcHitDamage(env, output, damageType) local convMult = output.conversionTable[damageType].mult min = min * convMult max = max * convMult if env.mode_effective then -- Apply resistances local preMult local taken = getMiscVal(modDB, "effective", damageType.."TakenInc", 0) + getMiscVal(modDB, "effective", "damageTakenInc", 0) if isElemental[damageType] then local resist = output["enemy_"..damageType.."Resist"] - sumMods(modDB, false, damageType.."Pen", "elementalPen") preMult = 1 - resist / 100 taken = taken + getMiscVal(modDB, "effective", "elementalTakenInc", 0) elseif damageType == "chaos" then preMult = 1 - output.enemy_chaosResist / 100 else preMult = 1 taken = taken - getMiscVal(modDB, "effective", "physicalRed", 0) end if env.skillSpaceFlags.projectile then taken = taken + getMiscVal(modDB, "effective", "projectileTakenInc", 0) end local mult = preMult * (1 + taken / 100) min = min * mult max = max * mult end else min, max = 0, 0 end output["total_"..damageType.."Min"] = min output["total_"..damageType.."Max"] = max output["total_"..damageType.."Avg"] = (min + max) / 2 endWatch(env, damageType) else min = output["total_"..damageType.."Min"] max = output["total_"..damageType.."Max"] end combMin = combMin + min combMax = combMax + max end output.total_combMin = combMin output.total_combMax = combMax -- Calculate crit chance, crit multiplier, and their combined effect if startWatch(env, "dps_crit") then if getMiscVal(modDB, nil, "neverCrit", false) then output.total_critChance = 0 output.total_critMultiplier = 0 output.total_critEffect = 1 else local baseCrit if isAttack then baseCrit = getMiscVal(modDB, "weapon1", "critChanceBase", 0) else baseCrit = getMiscVal(modDB, "skill", "critChanceBase", 0) end output.total_critChance = calcVal(modDB, "critChance", baseCrit) / 100 if env.mode_effective then output.total_critChance = output.total_critChance + getMiscVal(modDB, "effective", "additionalCritChance", 0) / 100 end if baseCrit < 100 then output.total_critChance = m_min(output.total_critChance, 0.95) end if baseCrit > 0 then output.total_critChance = m_max(output.total_critChance, 0.05) end if getMiscVal(modDB, nil, "noCritMult", false) then output.total_critMultiplier = 1 else output.total_critMultiplier = 1.5 + sumMods(modDB, false, "critMultiplier") / 100 end output.total_critEffect = 1 - output.total_critChance + output.total_critChance * output.total_critMultiplier end endWatch(env, "dps_crit") end -- Calculate skill speed if startWatch(env, "dps_speed") then if isAttack then local baseSpeed local attackTime = getMiscVal(modDB, "skill", "attackTime", 0) if attackTime > 0 then -- Skill is overriding weapon attack speed baseSpeed = 1 / attackTime * (1 + getMiscVal(modDB, "weapon1", "attackSpeedInc", 0) / 100) else baseSpeed = getMiscVal(modDB, "weapon1", "attackRate", 0) end output.total_speed = baseSpeed * (1 + sumMods(modDB, false, "speedInc", "attackSpeedInc") / 100) * sumMods(modDB, true, "speedMore", "attackSpeedMore") else local baseSpeed = 1 / getMiscVal(modDB, "skill", "castTime", 0) output.total_speed = baseSpeed * (1 + sumMods(modDB, false, "speedInc", "castSpeedInc") / 100) * sumMods(modDB, true, "speedMore", "castSpeedMore") end output.total_time = 1 / output.total_speed endWatch(env, "dps_speed") end -- Calculate hit chance if startWatch(env, "dps_hitChance") then if not isAttack or getMiscVal(modDB, "skill", "cannotBeEvaded", false) or getMiscVal(modDB, nil, "cannotBeEvaded", false) or getMiscVal(modDB, "weapon1", "cannotBeEvaded", false) then output.total_hitChance = 1 else output.total_accuracy = calcVal(modDB, "accuracy") local targetLevel = getMiscVal(modDB, "misc", "hitMonsterLevel", false) and m_min(getMiscVal(modDB, "monster", "level", 1), #data.enemyEvasionTable) or m_min(env.build.characterLevel, 79) local targetEvasion = data.enemyEvasionTable[targetLevel] if env.mode_effective then targetEvasion = targetEvasion * getMiscVal(modDB, "effective", "evasionMore", 1) end output.total_hitChance = calcHitChance(targetEvasion, output.total_accuracy) end endWatch(env, "dps_hitChance") end -- Calculate average damage and final DPS output.total_averageHit = (combMin + combMax) / 2 * output.total_critEffect output.total_averageDamage = output.total_averageHit * output.total_hitChance output.total_dps = output.total_averageDamage * output.total_speed -- Calculate mana cost (may be slightly off due to rounding differences) output.total_manaCost = m_floor(m_max(0, getMiscVal(modDB, "skill", "manaCostBase", 0) * (1 + sumMods(modDB, false, "manaCostInc") / 100) * sumMods(modDB, true, "manaCostMore") - sumMods(modDB, false, "manaCostBase"))) -- Calculate AoE stats if env.skillFlags.aoe then output.total_aoeRadiusMod = (1 + sumMods(modDB, false, "aoeRadiusInc") / 100) * sumMods(modDB, true, "aoeRadiusMore") end -- Calculate skill duration if startWatch(env, "duration") then local durationBase = getMiscVal(modDB, "skill", "durationBase", 0) output.total_durationMod = (1 + sumMods(modDB, false, "durationInc") / 100) * sumMods(modDB, true, "durationMore") if durationBase > 0 then output.total_duration = durationBase * output.total_durationMod end endWatch(env, "duration") end -- Calculate trap and mine stats stats if startWatch(env, "trapMine") then if env.skillFlags.trap then output.total_trapCooldown = getMiscVal(modDB, "skill", "trapCooldown", 4) / (1 + getMiscVal(modDB, nil, "trapCooldownRecoveryInc", 0) / 100) output.total_activeTrapLimit = sumMods(modDB, false, "activeTrapLimit") end if env.skillFlags.mine then output.total_activeMineLimit = sumMods(modDB, false, "activeMineLimit") end endWatch(env, "trapMine") end -- Calculate enemy stun modifiers if startWatch(env, "enemyStun") then local enemyStunThresholdRed = -sumMods(modDB, false, "stunEnemyThresholdInc") if enemyStunThresholdRed > 75 then output.stun_enemyThresholdMod = 1 - (75 + (enemyStunThresholdRed - 75) * 25 / (enemyStunThresholdRed - 50)) / 100 else output.stun_enemyThresholdMod = 1 - enemyStunThresholdRed / 100 end output.stun_enemyDuration = 0.35 * (1 + sumMods(modDB, false, "stunEnemyDurationInc") / 100) endWatch(env, "enemyStun") end -- Calculate skill DOT components output.total_damageDot = 0 for _, damageType in pairs(dmgTypeList) do if startWatch(env, damageType.."Dot", "enemyResist", "canDeal") then local baseVal if canDeal[damageType] then baseVal = getMiscVal(modDB, "skill", damageType.."DotBase", 0) else baseVal = 0 end if baseVal > 0 then env.skillFlags.dot = true buildSpaceTable(modDB, { dot = true, debuff = env.skillSpaceFlags.debuff, spell = getMiscVal(modDB, "skill", "dotIsSpell", false), projectile = env.skillSpaceFlags.projectile, aoe = env.skillSpaceFlags.aoe, totem = env.skillSpaceFlags.totem, trap = env.skillSpaceFlags.trap, mine = env.skillSpaceFlags.mine, }) local effMult = 1 if env.mode_effective then local preMult local taken = getMiscVal(modDB, "effective", damageType.."TakenInc", 0) + getMiscVal(modDB, "effective", "damageTakenInc", 0) + getMiscVal(modDB, "effective", "dotTakenInc", 0) if damageType == "physical" then taken = taken - getMiscVal(modDB, "effective", "physicalRed", 0) preMult = 1 else if isElemental[damageType] then taken = taken + getMiscVal(modDB, "effective", "elementalTakenInc", 0) end preMult = 1 - output["enemy_"..damageType.."Resist"] / 100 end effMult = preMult * (1 + taken / 100) end output["total_"..damageType.."Dot"] = baseVal * (1 + sumMods(modDB, false, "damageInc", damageType.."Inc", isElemental[damageType] and "elementalInc" or nil) / 100) * sumMods(modDB, true, "damageMore", damageType.."More", isElemental[damageType] and "elementalMore" or nil) * effMult end buildSpaceTable(modDB, env.skillSpaceFlags) endWatch(env, damageType.."Dot") end output.total_damageDot = output.total_damageDot + (output["total_"..damageType.."Dot"] or 0) end -- Calculate bleeding chance and damage env.skillFlags.bleed = false if startWatch(env, "bleed", "canDeal", "physical", "dps_crit") then output.bleed_chance = m_min(100, sumMods(modDB, false, "bleedChance")) / 100 if canDeal.physical and output.bleed_chance > 0 and output.total_physicalAvg > 0 then env.skillFlags.dot = true env.skillFlags.bleed = true env.skillFlags.duration = true buildSpaceTable(modDB, { dot = true, debuff = true, bleed = true, projectile = env.skillSpaceFlags.projectile, aoe = env.skillSpaceFlags.aoe, totem = env.skillSpaceFlags.totem, trap = env.skillSpaceFlags.trap, mine = env.skillSpaceFlags.mine, }) local baseVal = output.total_physicalAvg * output.total_critEffect * 0.1 local effMult = 1 if env.mode_effective then local taken = getMiscVal(modDB, "effective", "physicalTakenInc", 0) + getMiscVal(modDB, "effective", "damageTakenInc", 0) + getMiscVal(modDB, "effective", "dotTakenInc", 0) - getMiscVal(modDB, "effective", "physicalRed", 0) effMult = 1 + taken / 100 end output.bleed_dps = baseVal * (1 + sumMods(modDB, false, "damageInc", "physicalInc") / 100) * sumMods(modDB, true, "damageMore", "physicalMore") * effMult output.bleed_duration = 5 * (1 + sumMods(modDB, false, "durationInc") / 100) * sumMods(modDB, true, "durationMore") buildSpaceTable(modDB, env.skillSpaceFlags) end endWatch(env, "bleed") end -- Calculate poison chance and damage env.skillFlags.poison = false if startWatch(env, "poison", "canDeal", "physical", "chaos", "dps_crit", "enemyResist") then output.poison_chance = m_min(100, sumMods(modDB, false, "poisonChance")) / 100 if canDeal.chaos and output.poison_chance > 0 and (output.total_physicalAvg > 0 or output.total_chaosAvg > 0) then env.skillFlags.dot = true env.skillFlags.poison = true env.skillFlags.duration = true buildSpaceTable(modDB, { dot = true, debuff = true, spell = getMiscVal(modDB, "skill", "dotIsSpell", false), poison = true, projectile = env.skillSpaceFlags.projectile, aoe = env.skillSpaceFlags.aoe, totem = env.skillSpaceFlags.totem, trap = env.skillSpaceFlags.trap, mine = env.skillSpaceFlags.mine, }) local baseVal = (output.total_physicalAvg + output.total_chaosAvg) * output.total_critEffect * 0.08 local effMult = 1 if env.mode_effective then local taken = getMiscVal(modDB, "effective", "chaosTakenInc", 0) + getMiscVal(modDB, "effective", "damageTakenInc", 0) + getMiscVal(modDB, "effective", "dotTakenInc", 0) effMult = (1 - output["enemy_chaosResist"] / 100) * (1 + taken / 100) end output.poison_dps = baseVal * (1 + sumMods(modDB, false, "damageInc", "chaosInc") / 100) * sumMods(modDB, true, "damageMore", "chaosMore") * effMult output.poison_duration = 2 * (1 + sumMods(modDB, false, "durationInc") / 100) * sumMods(modDB, true, "durationMore") buildSpaceTable(modDB, env.skillSpaceFlags) end endWatch(env, "poison") end -- Calculate ignite chance and damage env.skillFlags.ignite = false if startWatch(env, "ignite", "canDeal", "fire", "cold", "dps_crit", "enemyResist") then output.ignite_chance = m_min(100, sumMods(modDB, false, "igniteChance")) / 100 local sourceDmg = 0 if canDeal.fire and not getMiscVal(modDB, nil, "fireCannotIgnite", false) then sourceDmg = sourceDmg + output.total_fireAvg end if canDeal.cold and getMiscVal(modDB, nil, "coldCanIgnite", false) then sourceDmg = sourceDmg + output.total_coldAvg end if canDeal.fire and output.ignite_chance > 0 and sourceDmg > 0 then env.skillFlags.dot = true env.skillFlags.ignite = true buildSpaceTable(modDB, { dot = true, debuff = true, spell = getMiscVal(modDB, "skill", "dotIsSpell", false), ignite = true, projectile = env.skillSpaceFlags.projectile, aoe = env.skillSpaceFlags.aoe, totem = env.skillSpaceFlags.totem, trap = env.skillSpaceFlags.trap, mine = env.skillSpaceFlags.mine, }) local baseVal = sourceDmg * output.total_critEffect * 0.2 local effMult = 1 if env.mode_effective then local taken = getMiscVal(modDB, "effective", "fireTakenInc", 0) + getMiscVal(modDB, "effective", "elementalTakenInc", 0) + getMiscVal(modDB, "effective", "damageTakenInc", 0) + getMiscVal(modDB, "effective", "dotTakenInc", 0) effMult = (1 - output["enemy_fireResist"] / 100) * (1 + taken / 100) end output.ignite_dps = baseVal * (1 + sumMods(modDB, false, "damageInc", "fireInc", "elementalInc") / 100) * sumMods(modDB, true, "damageMore", "fireMore", "elementalMore") * effMult output.ignite_duration = 4 * (1 + getMiscVal(modDB, "ignite", "durationInc", 0) / 100) buildSpaceTable(modDB, env.skillSpaceFlags) end endWatch(env, "ignite") end -- Calculate shock and freeze chance + duration modifier if startWatch(env, "shock", "canDeal", "lightning", "fire", "chaos") then output.shock_chance = m_min(100, sumMods(modDB, false, "shockChance")) / 100 local sourceDmg = 0 if canDeal.lightning and not getMiscVal(modDB, nil, "lightningCannotShock", false) then sourceDmg = sourceDmg + output.total_lightningAvg end if canDeal.fire and getMiscVal(modDB, nil, "fireCanShock", false) then sourceDmg = sourceDmg + output.total_fireAvg end if canDeal.chaos and getMiscVal(modDB, nil, "chaosCanShock", false) then sourceDmg = sourceDmg + output.total_chaosAvg end if output.shock_chance > 0 and sourceDmg > 0 then env.skillFlags.shock = true output.shock_durationMod = 1 + getMiscVal(modDB, "shock", "durationInc", 0) / 100 end end if startWatch(env, "freeze", "canDeal", "cold", "lightning") then output.freeze_chance = m_min(100, sumMods(modDB, false, "freezeChance")) / 100 local sourceDmg = 0 if canDeal.cold and not getMiscVal(modDB, nil, "coldCannotFreeze", false) then sourceDmg = sourceDmg + output.total_coldAvg end if canDeal.lightning and getMiscVal(modDB, nil, "lightningCanFreeze", false) then sourceDmg = sourceDmg + output.total_lightningAvg end if output.freeze_chance > 0 and sourceDmg > 0 then env.skillFlags.freeze = true output.freeze_durationMod = 1 + getMiscVal(modDB, "freeze", "durationInc", 0) / 100 end end -- Calculate combined DPS estimate, including DoTs output.total_combinedDPS = output[(env.mode_average and "total_averageDamage") or "total_dps"] + output.total_damageDot if env.skillFlags.poison then if env.mode_average then output.total_combinedDPS = output.total_combinedDPS + output.poison_chance * output.poison_dps * output.poison_duration else output.total_combinedDPS = output.total_combinedDPS + output.poison_chance * output.poison_dps * output.poison_duration * output.total_speed end end if env.skillFlags.ignite then output.total_combinedDPS = output.total_combinedDPS + output.ignite_dps end if env.skillFlags.bleed then output.total_combinedDPS = output.total_combinedDPS + output.bleed_dps end end local calcs = { } -- Wipe mod database and repopulate with base mods local function resetModDB(modDB, base) for spaceName, spaceMods in pairs(modDB) do local baseSpace = base[spaceName] if baseSpace then for k in pairs(spaceMods) do spaceMods[k] = baseSpace[k] end else wipeTable(spaceMods) end end end -- Print various tables to the console local function infoDump(env, output) ConPrintf("== Modifier Database ==") modLib.dbPrint(env.modDB) ConPrintf("== Main Skill ==") for _, gem in ipairs(env.mainSkill.validGemList) do ConPrintf("%s %d/%d", gem.name, gem.effectiveLevel, gem.effectiveQuality) end ConPrintf("== Main Skill Mods ==") modLib.listPrint(env.mainSkill.skillModList) ConPrintf("== Main Skill Flags ==") modLib.listPrint(env.skillFlags) ConPrintf("== Namespaces ==") modLib.listPrint(env.skillSpaceFlags) ConPrintf("== Aux Skills ==") for i, aux in ipairs(env.auxSkills) do ConPrintf("Skill #%d:", i) for _, gem in ipairs(aux.gemList) do ConPrintf(" %s %d/%d", gem.name, gem.effectiveLevel or gem.level, gem.effectiveQuality or gem.quality) end end --[[ConPrintf("== Buff Skill Mods ==") modLib.listPrint(env.buffSkillModList) ConPrintf("== Aura Skill Mods ==") modLib.listPrint(env.auraSkillModList) ConPrintf("== Curse Skill Mods ==") modLib.listPrint(env.curseSkillModList)]] if env.buffModList then ConPrintf("== Other Buff Mods ==") modLib.listPrint(env.buffModList) end ConPrintf("== Spec Mods ==") modLib.listPrint(env.specModList) ConPrintf("== Item Mods ==") modLib.listPrint(env.itemModList) ConPrintf("== Conditions ==") modLib.listPrint(env.condList) ConPrintf("== Conditional Modifiers ==") modLib.listPrint(env.condModList) ConPrintf("== Conversion Table ==") modLib.dbPrint(output.conversionTable) end -- Generate a function for calculating the effect of some modification to the environment local function getCalculator(build, input, fullInit, modFunc) -- Initialise environment local env = initEnv(build, input, "MAIN") -- Save a copy of the initial mod database if fullInit then mergeMainMods(env) end local initModDB = copyTable(env.modDB) if not fullInit then mergeMainMods(env) end -- Finialise modifier database and make a copy for later comparison local baseOutput = { } local outputMeta = { __index = baseOutput } finaliseMods(env, baseOutput) local baseModDB = copyTable(env.modDB) -- Run base calculation pass while building watch lists env.watchers = { } env.buildWatch = true env.modDB._activeWatchers = { } performCalcs(env, baseOutput) env.buildWatch = false env.modDB._activeWatchers = nil -- Generate list of watched mods local watchedModList = { } for _, watchList in pairs(env.watchers) do for k, default in pairs(watchList) do -- Add this watcher to the mod's watcher list local spaceName, modName = modLib.getSpaceName(k) if not watchedModList[spaceName] then watchedModList[spaceName] = { } end if not baseModDB[spaceName] then baseModDB[spaceName] = { } end if not initModDB[spaceName] then initModDB[spaceName] = { } end if not watchedModList[spaceName][modName] then watchedModList[spaceName][modName] = { baseModDB[spaceName][modName], { } } end watchedModList[spaceName][modName][2][watchList] = true if initModDB[spaceName][modName] == nil and baseModDB[spaceName][modName] ~= nil then -- Ensure that the initial mod list has at least a default value for any modifiers present in the base database initModDB[spaceName][modName] = default end end end local flagged = { } return function(...) -- Restore initial mod database resetModDB(env.modDB, initModDB) -- Call function to make modifications to the enviroment modFunc(env, ...) -- Prepare for calculation local output = setmetatable({ }, outputMeta) finaliseMods(env, output) --[[local debugThis = type(...) == "table" and #(...) == 1 and (...)[1].id == 17735 if debugThis then ConPrintf("+++++++++++++++++++++++++++++++++++++++") end]] -- Check if any watched mods have changed local active = false for spaceName, watchedMods in pairs(watchedModList) do for k, v in pairs(env.modDB[spaceName]) do local watchedMod = watchedMods[k] if watchedMod and v ~= watchedMod[1] then -- Modifier value has changed, flag all watchers for this mod for watchList in pairs(watchedMod[2]) do watchList._flag = true flagged[watchList] = true end --[[if debugThis then ConPrintf("%s:%s %s %s", spaceName, k, watchedMod[1], v) end]] active = true end end end if not active then return baseOutput end -- Run the calculations performCalcs(env, output) --[[if debugThis then infoDump(env, output) end]] -- Reset watcher flags for watchList in pairs(flagged) do watchList._flag = false flagged[watchList] = nil end return output end, baseOutput end -- Get calculator for tree node modifiers function calcs.getNodeCalculator(build, input) return getCalculator(build, input, true, function(env, nodeList, remove) -- Build and merge/unmerge modifiers for these nodes local nodeModList = buildNodeModList(env, nodeList) if remove then mod_dbUnmergeList(env.modDB, nodeModList) else mod_dbMergeList(env.modDB, nodeModList) end end) end -- Get calculator for item modifiers function calcs.getItemCalculator(build, input) return getCalculator(build, input, false, function(env, repSlotName, repItem) -- Merge main mods, replacing the item in the given slot with the given item mergeMainMods(env, repSlotName, repItem) end) end -- Build output for display in the grid or side bar function calcs.buildOutput(build, input, output, mode) -- Build output local env = initEnv(build, input, mode) mergeMainMods(env) finaliseMods(env, output) performCalcs(env, output) output.total_extraPoints = getMiscVal(env.modDB, nil, "extraPoints", 0) if mode == "GRID" then -- Add extra display-only stats for k, v in pairs(env.specModList) do output["spec_"..k] = v end for k, v in pairs(env.itemModList) do output["gear_"..k] = v end for i, aux in pairs(env.auxSkills) do output["buff_label"..i] = aux.displayLabel end for _, damageType in pairs(dmgTypeList) do -- Add damage ranges if output["total_"..damageType.."Max"] > 0 then output["total_"..damageType] = formatRound(output["total_"..damageType.."Min"]) .. " - " .. formatRound(output["total_"..damageType.."Max"]) else output["total_"..damageType] = 0 end end output.total_damage = formatRound(output.total_combMin) .. " - " .. formatRound(output.total_combMax) -- Calculate XP modifier if input.monster_level and input.monster_level > 0 then local playerLevel = build.characterLevel local diff = m_abs(playerLevel - input.monster_level) - 3 - m_floor(playerLevel / 16) if diff <= 0 then output.monster_xp = 1 else output.monster_xp = m_max(0.01, ((playerLevel + 5) / (playerLevel + 5 + diff ^ 2.5)) ^ 1.5) end end -- Configure view mode setViewMode(env, build.skillsTab.list) --infoDump(env, output) end end return calcs