diff --git a/src/Data/BuffStatMap.lua b/src/Data/BuffStatMap.lua new file mode 100644 index 000000000..b279d9f72 --- /dev/null +++ b/src/Data/BuffStatMap.lua @@ -0,0 +1,9 @@ +-- Path of Building +-- +-- Stat to internal modifier mapping table for buffs +-- Stat data (c) Grinding Gear Games +-- +local mod, flag, skill = ... + +return { +} \ No newline at end of file diff --git a/src/Data/Buffs/general.lua b/src/Data/Buffs/general.lua new file mode 100644 index 000000000..8ece6dbb2 --- /dev/null +++ b/src/Data/Buffs/general.lua @@ -0,0 +1,70 @@ +-- This file is automatically generated, do not edit! +-- Path of Building +-- +-- Buff data (c) Grinding Gear Games +-- +local buffs, mod, flag = ... + +buffs["archon_elemental"] = { + name = "Elemental Archon", + check="Condition:CanHaveElementalArchon", + condition="ElementalArchon", + statMap={ + ["elemental_damage_with_spell_skills_+%_final_from_archon_buff"]={ + mod("ElementalDamage", "MORE", nil, ModFlag.Spell), + }, + ["archon_spells_ignite_chance_+%_final"]={ + mod("EnemyIgniteChance", "MORE", nil, ModFlag.Spell), + }, + ["archon_spells_hit_damage_freeze_multiplier_+%_final"]={ + mod("EnemyFreezeBuildup", "MORE", nil, ModFlag.Spell), + }, + ["archon_spells_shock_chance_+%_final"]={ + mod("EnemyShockChance", "MORE", nil, ModFlag.Spell), + } + }, + incEffectMods={ + "ArchonEffect" + }, + durationIncMods={ + "ArchonDuration" + }, + ignoreIncEffectMods={ + }, + stats={ + "elemental_damage_with_spell_skills_+%_final_from_archon_buff", + "base_movement_velocity_+%", + "base_resist_all_elements_%", + "archon_spells_ignite_chance_+%_final", + "archon_spells_hit_damage_freeze_multiplier_+%_final", + "archon_spells_shock_chance_+%_final", + "base_critical_strike_multiplier_+", + "critical_strike_chance_+%", + }, + flags={ + "have_archon_elemental", + "deal_no_non_elemental_spell_damage", + "base_is_floating", + }, + grants={ + "elemental_damage_with_spell_skills_+%_final_from_archon_buff", + "base_movement_velocity_+%", + "base_resist_all_elements_%", + "archon_spells_ignite_chance_+%_final", + "archon_spells_hit_damage_freeze_multiplier_+%_final", + "archon_spells_shock_chance_+%_final", + "base_critical_strike_multiplier_+", + "critical_strike_chance_+%", + }, + values={ + {"buff_impl_stat",25}, + {"archon_grants_movement_speed_+%",0}, + {"archon_grants_all_elemental_resistance_+%",0}, + {"buff_impl_stat",100}, + {"buff_impl_stat",100}, + {"buff_impl_stat",100}, + {"archon_grants_base_critical_strike_multiplier_+",0}, + {"archon_grants_critical_strike_chance_+%",0}, + }, + duration = 10, +} \ No newline at end of file diff --git a/src/Data/Skills/sup_int.lua b/src/Data/Skills/sup_int.lua index 0460846bb..b47e24758 100644 --- a/src/Data/Skills/sup_int.lua +++ b/src/Data/Skills/sup_int.lua @@ -571,6 +571,11 @@ skills["SupportArbitersIgnitionPlayer"] = { label = "Arbiter's Ignition", incrementalEffectiveness = 0.054999999701977, statDescriptionScope = "gem_stat_descriptions", + statMap = { + ["gain_archon_elemental_when_you_ignite_enemy_chance_%"] = { + flag("Condition:CanHaveElementalArchon", { type = "GlobalEffect", effectType = "Buff" }), + }, + }, baseFlags = { }, constantStats = { diff --git a/src/Export/Buffs/general.txt b/src/Export/Buffs/general.txt new file mode 100644 index 000000000..29a4fa684 --- /dev/null +++ b/src/Export/Buffs/general.txt @@ -0,0 +1,32 @@ +-- Path of Building +-- +-- Buff data (c) Grinding Gear Games +-- +local buffs, mod, flag = ... + +#buff archon_elemental +#condition ElementalArchon +statMap={ + ["elemental_damage_with_spell_skills_+%_final_from_archon_buff"]={ + mod("ElementalDamage", "MORE", nil, ModFlag.Spell), + }, + ["archon_spells_ignite_chance_+%_final"]={ + mod("EnemyIgniteChance", "MORE", nil, ModFlag.Spell), + }, + ["archon_spells_hit_damage_freeze_multiplier_+%_final"]={ + mod("EnemyFreezeBuildup", "MORE", nil, ModFlag.Spell), + }, + ["archon_spells_shock_chance_+%_final"]={ + mod("EnemyShockChance", "MORE", nil, ModFlag.Spell), + } +}, +incEffectMods={ + "ArchonEffect" +}, +durationIncMods={ + "ArchonDuration" +}, +ignoreIncEffectMods={ +}, +#stats +#buffEnd \ No newline at end of file diff --git a/src/Export/Scripts/buff.lua b/src/Export/Scripts/buff.lua new file mode 100644 index 000000000..52fd4b7b8 --- /dev/null +++ b/src/Export/Scripts/buff.lua @@ -0,0 +1,89 @@ +if not loadStatFile then + dofile("statdesc.lua") +end +loadStatFile("stat_descriptions.csd") + +local directiveTable = { } + +-- #Buff +directiveTable.buff = function(state, args, out) + state.currentBuffId = args + local bufDefinitionID = dat("buffdefinitions"):GetRow("Id", state.currentBuffId) + if not bufDefinitionID then + error("BuffDefinitionId '" .. state.currentBuffId .. "' not found in database") + end + state.currentBuffDefinitionId = bufDefinitionID + out:write('buffs["', state.currentBuffId, '"] = {\n') + + -- print name + if bufDefinitionID.Name then + out:write('\tname = "', bufDefinitionID.Name:gsub('"', '\\"'), '",\n') + end +end + +directiveTable.buffEnd = function(state, args, out) + state.currentBuffId = nil + state.currentBuffDefinitionId = nil + out:write('}') +end + +-- #condition +-- build "Condition:" and "Condition:CanHave" +directiveTable.condition = function(state, args, out) + out:write('\tcheck="Condition:CanHave' .. args .. '",\n') + out:write('\tcondition="' .. args .. '",\n') +end + +-- #stats +directiveTable.stats = function(state, args, out) + if not state.currentBuffDefinitionId.Stats then + error("BuffDefinitionId '" .. state.currentBuffId .. "' has no associated stats") + end + + print("Writing stats for buff " .. state.currentBuffId) + local stats = state.currentBuffDefinitionId.Stats + local flags = state.currentBuffDefinitionId.GrantedFlags + local grantStats = state.currentBuffDefinitionId.GrantedStats + + out:write('\tstats={\n') + for k, stat in ipairs(stats) do + print(" Writing stat " .. stat.Id) + out:write('\t\t"' .. stat.Id .. '",\n') + end + out:write('\t},\n') + + out:write('\tflags={\n') + for k, stat in ipairs(flags) do + print(" Writing flag " .. stat.Id) + out:write('\t\t"' .. stat.Id .. '",\n') + end + out:write('\t},\n') + + out:write('\tgrants={\n') + for k, stat in ipairs(grantStats) do + print(" Writing granted stat " .. stat.Id) + out:write('\t\t"' .. stat.Id .. '",\n') + end + out:write('\t},\n') + + -- now search for bufftemplates that reference this buffdefinition + print("Searching for buff templates for buff " .. state.currentBuffId) + local buffTemplates = dat("bufftemplates"):GetRow("BuffDefinition", state.currentBuffDefinitionId) + if buffTemplates then + out:write('\tvalues={\n') + for k, stat in ipairs(buffTemplates.Stats) do + print(" Writing template stat " .. stat.Id) + out:write('\t\t{"' .. stat.Id .. '",' .. buffTemplates.StatValues[k] .. '}') + out:write(',\n') + end + out:write('\t},\n') + -- enable duration + out:write('\tduration = ', buffTemplates.Duration, ',\n') + end +end + +for _, name in pairs({"general"}) do + processTemplateFile(name, "Buffs/", "../Data/Buffs/", directiveTable) +end + +print("Buff data exported.") \ No newline at end of file diff --git a/src/Export/Skills/sup_int.txt b/src/Export/Skills/sup_int.txt index 53d2b01c3..33b912d73 100644 --- a/src/Export/Skills/sup_int.txt +++ b/src/Export/Skills/sup_int.txt @@ -89,6 +89,11 @@ statMap = { #skill SupportArbitersIgnitionPlayer #set SupportArbitersIgnitionPlayer +statMap = { + ["gain_archon_elemental_when_you_ignite_enemy_chance_%"] = { + flag("Condition:CanHaveElementalArchon", { type = "GlobalEffect", effectType = "Buff" }), + }, +}, #mods #skillEnd diff --git a/src/Modules/CalcPerform.lua b/src/Modules/CalcPerform.lua index fc2cb2c32..dfd89dc4e 100644 --- a/src/Modules/CalcPerform.lua +++ b/src/Modules/CalcPerform.lua @@ -17,6 +17,38 @@ local m_modf = math.modf local s_format = string.format local m_huge = math.huge +-- Merge level modifier with given mod list +local mergeLevelCache = { } +local function mergeLevelMod(modList, mod, value, scale) + if not value then + modList:AddMod(mod) + return + end + if not mergeLevelCache[mod] then + mergeLevelCache[mod] = { } + end + if mergeLevelCache[mod][value] then + modList:AddMod(mergeLevelCache[mod][value]) + elseif value then + local newMod = copyTable(mod, true) + if type(newMod.value) == "table" then + newMod.value = copyTable(newMod.value, true) + if newMod.value.mod then + newMod.value.mod = copyTable(newMod.value.mod, true) + newMod.value.mod.value = value + else + newMod.value.value = value + end + else + newMod.value = value + end + mergeLevelCache[mod][value] = newMod + modList:ScaleAddMod(newMod, scale) + else + modList:ScaleAddMod(mod, scale) + end +end + --- getCachedOutputValue --- retrieves a value specified by key from a cached version of skill --- specified by @uuid or if not found in cache computes teh cache. @@ -696,6 +728,67 @@ local function doActorMisc(env, actor) local max = modDB:Override(nil, "SoulEaterMax") or modDB:Sum("BASE", nil, "SoulEaterMax") modDB:NewMod("Multiplier:SoulEater", "BASE", 1, "Base", { type = "Multiplier", var = "SoulEaterStack", limit = max }) end + + -- Build the list of buff base on conditions "Condition:CanHave" + for buffId, buffInfo in pairs(env.data.buffs) do + if modDB:Flag(nil, buffInfo.check) then + -- add buff stats + local modBuffList = new("ModList") + + -- calculate inc Effect modifier + local buffEffectMod = 1 + local incSum = 0 + for _, incEffect in ipairs(buffInfo.incEffectMods or {}) do + incSum = incSum + modDB:Sum("INC", nil, incEffect) + end + buffEffectMod = buffEffectMod * (1 + incSum / 100) + + -- calculate duration modifier + local durationIncSum = 0 + for _, durationInc in ipairs(buffInfo.durationIncMods or {}) do + durationIncSum = durationIncSum + modDB:Sum("INC", nil, durationInc) + end + + -- add mods base for duration + modBuffList:NewMod(buffInfo.condition .. "Duration", "BASE", buffInfo.duration, "Base Duration") + + env.player.output[buffInfo.condition .. "Duration"] = m_floor(buffInfo.duration * (1 + durationIncSum / 100)) + env.player.output[buffInfo.condition .. "IncEffect"] = buffEffectMod * 100 + + for k, stat in ipairs(buffInfo.stats) do + local statValue = buffInfo.values and buffInfo.values[k] and buffInfo.values[k][2] or 0 + local map = buffInfo.statMap[stat] + if map then + -- Some mods need different scalars for different stats, but the same value. Putting them in a group allows this + for _, mod in ipairs(map) do + -- Found a mod, since all mods have names + local modOrGroup = copyTable(mod) + + -- we should add the conditional flag for each mod coming from a buff + table.insert(modOrGroup, { type = "Condition", var = buffInfo.condition }) + + -- add multiplier for buff effect + local modIncBuffEffect = 1 + if modOrGroup.name~= nil or buffInfo.ignoreIncEffectMods[modOrGroup.name] ~= true then + modIncBuffEffect = buffEffectMod + end + + if modOrGroup.name then + modOrGroup.source = string.format("Buff:%s", buffId) + mergeLevelMod(modBuffList, modOrGroup, map.value or statValue * (map.mult or 1) / (map.div or 1) + (map.base or 0), modIncBuffEffect) + else + for _, mod in ipairs(modOrGroup) do + mod.source = string.format("Buff:%s", buffId) + mergeLevelMod(modBuffList, mod, modOrGroup.value or statValue * (modOrGroup.mult or 1) / (modOrGroup.div or 1) + (modOrGroup.base or 0), modIncBuffEffect) + end + end + end + end + end + + modDB:AddList(modBuffList) + end + end end -- Process enemy modifiers diff --git a/src/Modules/CalcSections.lua b/src/Modules/CalcSections.lua index 992684637..5fd1ad34a 100644 --- a/src/Modules/CalcSections.lua +++ b/src/Modules/CalcSections.lua @@ -1431,6 +1431,13 @@ return { { breakdown = "OffHand.KnockbackDistance" }, { modName = "EnemyKnockbackDistance", cfg = "weapon2" }, }, }, + { label = "Archon Effect", haveOutput = "ElementalArchonIncEffect", { format = "{0:output:ElementalArchonIncEffect}%", + { modName = "ArchonEffect"}, + }, }, + { label = "Archon Duration", haveOutput = "ElementalArchonDuration", { format = "{2:output:ElementalArchonDuration}s", + { modName = "ElementalArchonDuration"}, + { modName = "ArchonDuration"}, + }, }, { label = "Presence Mod", haveOutput = "PresenceMod", { format = "{2:output:PresenceMod}", { breakdown = "PresenceMod" }, { modName = "PresenceRadius", cfg = "skill" }} , }, { label = "Presence Radius", haveOutput = "PresenceRadius", { format = "{1:output:PresenceRadiusMetres}m", { breakdown = "PresenceRadius" }, { modName = "PresenceArea", cfg = "skill"} }, }, { label = "Surrounded Mod", haveOutput = "SurroundedMod", { format = "{2:output:SurroundedMod}", { breakdown = "SurroundedMod" }, { modName = "SurroundedRadius", cfg = "skill" }} , }, diff --git a/src/Modules/Calcs.lua b/src/Modules/Calcs.lua index 7f06f8bd6..aca8c3077 100644 --- a/src/Modules/Calcs.lua +++ b/src/Modules/Calcs.lua @@ -704,6 +704,11 @@ function calcs.buildOutput(build, mode) if env.modDB:Flag(nil, "Elusive") then t_insert(combatList, "Elusive") end + for buffId, buffInfo in pairs(env.data.buffs) do + if env.modDB:Flag(nil, buffInfo.check) then + t_insert(combatList, buffInfo.name) + end + end table.sort(buffList) env.player.breakdown.SkillBuffs = { modList = { } } for _, name in ipairs(buffList) do diff --git a/src/Modules/ConfigOptions.lua b/src/Modules/ConfigOptions.lua index f48539bbe..c791ea35a 100644 --- a/src/Modules/ConfigOptions.lua +++ b/src/Modules/ConfigOptions.lua @@ -849,6 +849,10 @@ Huge sets the radius to 11. { var = "buffArcaneSurge", type = "check", label = "Do you have Arcane Surge?", tooltip = "In addition to allowing any 'while you have Arcane Surge' modifiers to apply,\nthis will enable the Arcane Surge buff itself. (Grants 15% increased Cast Speed and 20% more Mana Regeneration rate)", apply = function(val, modList, enemyModList) modList:NewMod("Condition:ArcaneSurge", "FLAG", true, "Config", { type = "Condition", var = "Combat" }) end }, + { var = "buffElementalArchon", type = "check", label = "Do you have Elemental Archon?", ifFlag = "Condition:CanHaveElementalArchon", tooltip = "", apply = function(val, modList, enemyModList) + modList:NewMod("Condition:ElementalArchon", "FLAG", true, "Config", { type = "Condition", var = "Combat" }) + modList:NewMod("Condition:ArchonBuff", "FLAG", true, "Config", { type = "Condition", var = "Combat" }) + end }, { var = "buffQuicksandHourglass", type = "check", label = "Do you have Quicksand Hourglass?", ifFlag = "Condition:CanGainQuicksandHourglass", tooltip = "this will enable the Quicksand Hourglass buff itself.", apply = function(val, modList, enemyModList) modList:NewMod("Condition:QuicksandHourglass", "FLAG", true, "Config", { type = "Condition", var = "Combat" }) end }, diff --git a/src/Modules/Data.lua b/src/Modules/Data.lua index 2fd50ca0f..cc47fa8f9 100644 --- a/src/Modules/Data.lua +++ b/src/Modules/Data.lua @@ -23,6 +23,9 @@ local skillTypes = { "sup_dex", "sup_int", } +local buffTypes = { + "general", +} local itemTypes = { "axe", "bow", @@ -95,6 +98,12 @@ local function processMod(grantedEffect, mod, statName) t_insert(mod, { type = "ActorCondition", actor = "parent", neg = true }) end end +local function processBuffMod(buff, mod, statName) + mod.source = "Buff:"..buff.name + if type(mod.value) == "table" and mod.value.mod then + mod.value.mod.source = "Buff:"..buff.name + end +end ----------------- -- Common Data -- @@ -820,6 +829,49 @@ Uber Pinnacle Boss adds the following modifiers: ]]..tostring(data.misc.uberBossPen)..[[% penetration]] end +-- load buffs +data.buffs = {} +data.buffStatMap = LoadModule("Data/BuffStatMap", makeSkillMod, makeFlagMod) +data.buffStatMapMeta = { + __index = function(t, key) + local map = data.buffStatMap[key] + if map then + map = copyTable(map) + t[key] = map + for _, mod in ipairs(map) do + processBuffMod(t._buff, mod, key) + end + return map + end + end +} +for _, type in pairs(buffTypes) do + LoadModule("Data/Buffs/"..type, data.buffs, makeSkillMod, makeFlagMod) +end +for buffId, buffInfo in pairs(data.buffs) do + local buff = {} + buff.name = sanitiseText(buffInfo.name) + buff.id = buffId + buff.modSource = "Buff:"..buff.name + buffInfo.statMap = buffInfo.statMap or {} + + setmetatable(buffInfo.statMap, data.buffStatMapMeta) + buffInfo.statMap._buff = buff + + for name, map in pairs(buffInfo.statMap) do + -- Some mods need different scalars for different stats, but the same value. Putting them in a group allows this + for _, modOrGroup in ipairs(map) do + if modOrGroup.name then + processBuffMod(buff, modOrGroup, name) + else + for _, mod in ipairs(modOrGroup) do + processBuffMod(buff, mod, name) + end + end + end + end +end + -- Load skills data.skills = { } data.skillStatMap = LoadModule("Data/SkillStatMap", makeSkillMod, makeFlagMod, makeSkillDataMod) diff --git a/src/Modules/ModParser.lua b/src/Modules/ModParser.lua index 798df8c08..828b6d99b 100644 --- a/src/Modules/ModParser.lua +++ b/src/Modules/ModParser.lua @@ -2099,6 +2099,7 @@ local modTagList = { ["per poison affecting enemy"] = { tag = { type = "Multiplier", actor = "enemy", var = "PoisonStack" } }, ["per poison affecting enemy, up to %+([%d%.]+)%%"] = function(num) return { tag = { type = "Multiplier", actor = "enemy", var = "PoisonStack", limit = num, limitTotal = true } } end, ["for each spider's web on the enemy"] = { tag = { type = "Multiplier", actor = "enemy", var = "Spider's WebStack" } }, + ["while affected by an archon buff"] = { tag = { type = "Condition", var = "ArchonBuff" } }, } local mod = modLib.createMod @@ -5923,6 +5924,41 @@ local specialModList = { ["nearby allies have (%d+)%% chance to block attack damage per (%d+) strength you have"] = function(block, _, str) return { mod("ExtraAura", "LIST", { onlyAllies = true, mod = mod("BlockChance", "BASE", block) }, { type = "PerStat", stat = "Str", div = tonumber(str) }), } end, + -- archon modifiers + ["gain elemental archon when your energy shield recharge begins"] = { + flag("Condition:CanHaveElementalArchon"), + }, + ["gain elemental archon when you cast a spell while on high infernal flame"] = { + flag("Condition:CanHaveElementalArchon"), + }, + ["gain elemental archon after spending 100%% of your maximum mana"] = { + flag("Condition:CanHaveElementalArchon"), + }, + ["immune to freeze and chill while affected by an archon buff"] = { + flag("FreezeImmune", { type = "Condition", var = "ArchonBuff" }), + flag("ChillImmune", { type = "Condition", var = "ArchonBuff" }), + }, + ["immune to shock while affected by an archon buff"] = { + flag("ShockImmune", { type = "Condition", var = "ArchonBuff" }), + }, + ["(%d+)%% increased effect of archon buffs on you"] = function(num) return { + mod("ArchonEffect", "INC", num), + } end, + ["(%d+)%% increased archon buff duration"] = function(num) return { + mod("ArchonDuration", "INC", num), + } end, + ["archon buffs also grant %+(%d+)%% to all elemental resistances"] = function(num) return { + mod("ElementalResist", "BASE", num, { type = "Condition", var = "ArchonBuff" }, { type="PercentStat", stat="ElementalArchonIncEffect", div=100, percent=1 }), + } end, + ["archon buffs also grant (%d+)%% increased movement speed"] = function(num) return { + mod("MovementSpeed", "INC", num, { type = "Condition", var = "ArchonBuff" }, { type="PercentStat", stat="ElementalArchonIncEffect", div=100, percent=1 }), + } end, + ["archon buffs also grant %+(%d+)%% critical damage bonus"] = function(num) return { + mod("CritMultiplier", "INC", num, { type = "Condition", var = "ArchonBuff" }, { type="PercentStat", stat="ElementalArchonIncEffect", div=100, percent=1 }), + } end, + ["archon buffs also grant %+(%d+)%% critical hit chance"] = function(num) return { + mod("CritChance", "INC", num, { type = "Condition", var = "ArchonBuff" }, { type="PercentStat", stat="ElementalArchonIncEffect", div=100, percent=1 }), + } end, } for _, name in pairs(data.keystones) do specialModList[name:lower()] = { mod("Keystone", "LIST", name) }