From e1161aae96852d7f325a544bfb04770da7112e40 Mon Sep 17 00:00:00 2001 From: Leonel Togniolli Date: Tue, 9 Jun 2026 22:48:47 -0300 Subject: [PATCH 1/3] Add support for tamed beast modifiers on Companion gems Rare tamed beasts carry randomly rolled monster modifiers, returned by the character API as "tamedBeastProperties". These were previously discarded on import, so companions were modeled only as their base monster. - Import: parse tamedBeastProperties into a per-gem modifier list, resolving mod ids from "[ModId |Display]" tokens with a display-line fallback for nameplate-only lines; user enable/disable choices survive re-import - Data: new generated Data/TamedBeastMods.lua (export script iterates the Monster-domain mod pool, naming entries from KeywordPopups/ArchnemesisMods and resolving effects through SkillStatMap); mods with a positive spawn weight are flagged rollable and offered in the UI - Calcs: enabled modifiers apply to the companion minion's modDB, gated on the active skill being a Companion so stale lists never leak - UI: "Beast Modifiers" section in the socket group editor with per-row selection dropdown, enable checkbox, remove button and calc-comparison tooltips, mirroring the gem slot layout - Persistence: modifiers saved as TamedBeastMod child elements of Gem; old builds are unaffected --- spec/System/TestTamedBeastMods_spec.lua | 254 +++ src/Classes/ImportTab.lua | 41 + src/Classes/SkillsTab.lua | 250 +++ src/Data/TamedBeastMods.lua | 2180 +++++++++++++++++++++++ src/Export/Scripts/tamedBeastMods.lua | 177 ++ src/Modules/CalcPerform.lua | 14 + src/Modules/Data.lua | 34 + 7 files changed, 2950 insertions(+) create mode 100644 spec/System/TestTamedBeastMods_spec.lua create mode 100644 src/Data/TamedBeastMods.lua create mode 100644 src/Export/Scripts/tamedBeastMods.lua diff --git a/spec/System/TestTamedBeastMods_spec.lua b/spec/System/TestTamedBeastMods_spec.lua new file mode 100644 index 0000000000..501c5d6faf --- /dev/null +++ b/spec/System/TestTamedBeastMods_spec.lua @@ -0,0 +1,254 @@ +describe("TestTamedBeastMods", function() + before_each(function() + newBuild() + end) + + local sampleProperties = { + { + name = "Monster Modifiers:\n{0}", + values = { + { "[MonsterFlaskRemovalAura1|Siphons Flask Charges]\n[MonsterLifeRegenerationRatePercentage1|Regenerates Life]\nPeriodically unleashes [Cold|Ice]\n[MonsterAdditionalProjectiles1|Additional Projectiles]", 0 }, + }, + displayMode = 3, + }, + } + + describe("ImportTab.ParseTamedBeastProperties", function() + it("parses mod-id tokens and resolves display-only lines", function() + local list = build.importTab:ParseTamedBeastProperties(sampleProperties) + + assert.are.equals(4, #list) + assert.are.equals("MonsterFlaskRemovalAura1", list[1].modId) + assert.are.equals("Siphons Flask Charges", list[1].display) + assert.are.equals("MonsterLifeRegenerationRatePercentage1", list[2].modId) + -- No leading [ModId|...] token; resolved by display line lookup + assert.are.equals("Periodically unleashes Ice", list[3].display) + assert.is_not_nil(list[3].modId) + assert.are.equals("MonsterAdditionalProjectiles1", list[4].modId) + for _, entry in ipairs(list) do + assert.True(entry.enabled) + end + end) + + it("keeps unresolvable lines with display text only", function() + local list = build.importTab:ParseTamedBeastProperties({ + { values = { { "[MonsterMadeUpModDoesNotExist1|Made Up Mod]", 0 } } }, + }) + + assert.are.equals(1, #list) + assert.is_nil(list[1].modId) + assert.are.equals("Made Up Mod", list[1].display) + end) + + it("returns nil for absent or empty input", function() + assert.is_nil(build.importTab:ParseTamedBeastProperties(nil)) + assert.is_nil(build.importTab:ParseTamedBeastProperties({ })) + end) + end) + + describe("SkillsTab persistence", function() + it("saves tamed beast mods as Gem child elements", function() + build.skillsTab.skillSets[1].socketGroupList = { { + enabled = true, + gemList = { { + nameSpec = "Companion: Mighty Silverfist", + level = 20, quality = 0, enabled = true, enableGlobal1 = true, enableGlobal2 = true, + count = 1, corrupted = false, corruptLevel = 0, + tamedBeastModList = { + { modId = "MonsterDamageGainedAsCold1", enabled = true }, + { display = "Periodically unleashes Ice", enabled = false }, + }, + } }, + } } + + local xml = { } + build.skillsTab:Save(xml) + + local gemNode + for _, skillSetNode in ipairs(xml) do + if skillSetNode.elem == "SkillSet" then + for _, skillNode in ipairs(skillSetNode) do + if skillNode.elem == "Skill" then + gemNode = skillNode[1] + end + end + end + end + assert.is_not_nil(gemNode) + assert.are.equals("Gem", gemNode.elem) + + local beastModNodes = { } + for _, child in ipairs(gemNode) do + if child.elem == "TamedBeastMod" then + table.insert(beastModNodes, child) + end + end + assert.are.equals(2, #beastModNodes) + assert.are.equals("MonsterDamageGainedAsCold1", beastModNodes[1].attrib.modId) + assert.are.equals("true", beastModNodes[1].attrib.enabled) + assert.is_nil(beastModNodes[2].attrib.modId) + assert.are.equals("Periodically unleashes Ice", beastModNodes[2].attrib.display) + assert.are.equals("false", beastModNodes[2].attrib.enabled) + end) + + it("loads TamedBeastMod elements back onto the gem instance", function() + local node = { elem = "Skill", attrib = { enabled = "true" }, + { elem = "Gem", attrib = { nameSpec = "Companion: Mighty Silverfist", level = "20", quality = "0", enabled = "true" }, + { elem = "TamedBeastMod", attrib = { modId = "MonsterDamageGainedAsCold1", enabled = "true" } }, + { elem = "TamedBeastMod", attrib = { display = "Periodically unleashes Ice", enabled = "false" } }, + }, + } + + build.skillsTab:LoadSkill(node, 1) + + local socketGroupList = build.skillsTab.skillSets[1].socketGroupList + local gemInstance = socketGroupList[#socketGroupList].gemList[1] + assert.is_not_nil(gemInstance.tamedBeastModList) + assert.are.equals(2, #gemInstance.tamedBeastModList) + assert.are.equals("MonsterDamageGainedAsCold1", gemInstance.tamedBeastModList[1].modId) + assert.True(gemInstance.tamedBeastModList[1].enabled) + assert.is_nil(gemInstance.tamedBeastModList[2].modId) + assert.are.equals("Periodically unleashes Ice", gemInstance.tamedBeastModList[2].display) + assert.False(gemInstance.tamedBeastModList[2].enabled) + end) + + it("loads legacy gems without beast mods as nil", function() + local node = { elem = "Skill", attrib = { enabled = "true" }, + { elem = "Gem", attrib = { nameSpec = "Fireball", level = "20", quality = "0", enabled = "true" } }, + } + + build.skillsTab:LoadSkill(node, 1) + + local socketGroupList = build.skillsTab.skillSets[1].socketGroupList + assert.is_nil(socketGroupList[#socketGroupList].gemList[1].tamedBeastModList) + end) + end) + + describe("Calculation wiring", function() + local beastId = "Metadata/Monsters/Quadrilla/QuadrillaBossMinion1" -- Mighty Silverfist + + local function buildCompanionGroup(tamedBeastModList) + table.insert(build.beastList, beastId) + local gemInstance = { + nameSpec = "Companion: Mighty Silverfist", + gemId = "Metadata/Items/Gems/SkillGemSummonBeast", + level = 20, quality = 0, enabled = true, enableGlobal1 = true, enableGlobal2 = true, + count = 1, corrupted = false, corruptLevel = 0, + skillMinion = beastId, + skillMinionCalcs = beastId, + tamedBeastModList = tamedBeastModList, + } + local group = { label = "", enabled = true, gemList = { gemInstance } } + table.insert(build.skillsTab.socketGroupList, group) + build.skillsTab:ProcessSocketGroup(group) + build.mainSocketGroup = #build.skillsTab.socketGroupList + build.buildFlag = true + runCallback("OnFrame") + return gemInstance + end + + it("applies enabled beast mods to the companion minion", function() + buildCompanionGroup({ { modId = "MonsterDamageGainedAsCold1", enabled = true } }) + + local minion = build.calcsTab.mainEnv.minion + assert.is_not_nil(minion) + assert.are.equals(40, minion.modDB:Sum("BASE", nil, "DamageGainAsCold")) + end) + + it("skips disabled and unresolved beast mods", function() + local gemInstance = buildCompanionGroup({ + { modId = "MonsterDamageGainedAsCold1", enabled = false }, + { display = "Periodically unleashes Ice", enabled = true }, + }) + + local minion = build.calcsTab.mainEnv.minion + assert.is_not_nil(minion) + assert.are.equals(0, minion.modDB:Sum("BASE", nil, "DamageGainAsCold")) + + gemInstance.tamedBeastModList[1].enabled = true + build.buildFlag = true + runCallback("OnFrame") + assert.are.equals(40, build.calcsTab.mainEnv.minion.modDB:Sum("BASE", nil, "DamageGainAsCold")) + end) + + it("does not affect companions without beast mods", function() + buildCompanionGroup(nil) + + local minion = build.calcsTab.mainEnv.minion + assert.is_not_nil(minion) + assert.are.equals(0, minion.modDB:Sum("BASE", nil, "DamageGainAsCold")) + end) + + it("does not apply a stale beast mod list when the gem is not a Companion", function() + build.skillsTab:PasteSocketGroup("Skeletal Sniper 20/0 1") + runCallback("OnFrame") + local srcInstance = build.skillsTab.socketGroupList[1].gemList[1] + srcInstance.tamedBeastModList = { { modId = "MonsterDamageGainedAsCold1", enabled = true } } + build.buildFlag = true + runCallback("OnFrame") + + local minion = build.calcsTab.mainEnv.minion + assert.is_not_nil(minion) + assert.are.equals(0, minion.modDB:Sum("BASE", nil, "DamageGainAsCold")) + end) + + it("applies entries beyond the fourth and creates UI rows for them", function() + buildCompanionGroup({ + { modId = "MonsterIncreasedSpeedAura1", enabled = false }, + { modId = "MonsterLifeRegenerationRatePercentage1", enabled = false }, + { modId = "MonsterAdditionalProjectiles1", enabled = false }, + { modId = "MonsterFlaskRemovalAura1", enabled = false }, + { modId = "MonsterDamageGainedAsCold1", enabled = true }, + }) + + assert.are.equals(40, build.calcsTab.mainEnv.minion.modDB:Sum("BASE", nil, "DamageGainAsCold")) + + local skillsTab = build.skillsTab + skillsTab:SetDisplayGroup(skillsTab.socketGroupList[#skillsTab.socketGroupList]) + skillsTab:UpdateBeastModSlots() + assert.is_not_nil(skillsTab.beastModSlots[6]) + local slot5 = skillsTab.beastModSlots[5] + assert.are.equals("MonsterDamageGainedAsCold1", slot5.select.list[slot5.select.selIndex].modId) + end) + end) + + describe("Re-import preservation", function() + local function companionPayload() + return { + level = 12, + equipment = { }, + skills = { { + support = false, + typeLine = "Companion: Mighty Silverfist", + properties = { { name = "Level", values = { { "17", 0 } } } }, + tamedBeastProperties = { { + name = "Monster Modifiers:\n{0}", + values = { { "[MonsterDamageGainedAsCold1|Extra Cold Damage]\n[MonsterIncreasedSpeedAura1|Haste Aura]", 0 } }, + displayMode = 3, + } }, + } }, + } + end + + it("keeps user enable choices for surviving mods when re-importing", function() + build.importTab.controls.charImportItemsClearSkills.state = true + build.importTab.controls.charImportItemsClearItems.state = false + build.importTab:ImportItemsAndSkills(companionPayload()) + runCallback("OnFrame") + + local gem = build.skillsTab.socketGroupList[1].gemList[1] + assert.are.equals(2, #gem.tamedBeastModList) + assert.True(gem.tamedBeastModList[1].enabled) + gem.tamedBeastModList[1].enabled = false + + build.importTab:ImportItemsAndSkills(companionPayload()) + runCallback("OnFrame") + + gem = build.skillsTab.socketGroupList[1].gemList[1] + assert.are.equals(2, #gem.tamedBeastModList) + assert.are.equals("MonsterDamageGainedAsCold1", gem.tamedBeastModList[1].modId) + assert.False(gem.tamedBeastModList[1].enabled) + assert.True(gem.tamedBeastModList[2].enabled) + end) + end) +end) diff --git a/src/Classes/ImportTab.lua b/src/Classes/ImportTab.lua index 0dcb9c59d9..b1c56ae8cf 100644 --- a/src/Classes/ImportTab.lua +++ b/src/Classes/ImportTab.lua @@ -859,6 +859,7 @@ local function snapshotSocketGroupReimportState(socketGroup, isMainGroup) skillMinionSkillCalcs = gem.skillMinionSkillCalcs, skillMinionSkillStatSetIndexLookup = gem.skillMinionSkillStatSetIndexLookup and copyTable(gem.skillMinionSkillStatSetIndexLookup), skillMinionSkillStatSetIndexLookupCalcs = gem.skillMinionSkillStatSetIndexLookupCalcs and copyTable(gem.skillMinionSkillStatSetIndexLookupCalcs), + tamedBeastModList = gem.tamedBeastModList and copyTable(gem.tamedBeastModList), enableGlobal1 = gem.enableGlobal1, enableGlobal2 = gem.enableGlobal2, } @@ -894,6 +895,23 @@ local function applyGemReimportState(gem, state) gem.skillMinionSkillCalcs = state.skillMinionSkillCalcs gem.skillMinionSkillStatSetIndexLookup = state.skillMinionSkillStatSetIndexLookup and copyTable(state.skillMinionSkillStatSetIndexLookup) gem.skillMinionSkillStatSetIndexLookupCalcs = state.skillMinionSkillStatSetIndexLookupCalcs and copyTable(state.skillMinionSkillStatSetIndexLookupCalcs) + if state.tamedBeastModList then + if gem.tamedBeastModList then + local preservedEnabled = { } + for _, entry in ipairs(state.tamedBeastModList) do + if entry.modId and entry.enabled ~= nil then + preservedEnabled[entry.modId] = entry.enabled + end + end + for _, entry in ipairs(gem.tamedBeastModList) do + if entry.modId and preservedEnabled[entry.modId] ~= nil then + entry.enabled = preservedEnabled[entry.modId] + end + end + else + gem.tamedBeastModList = copyTable(state.tamedBeastModList) + end + end gem.enableGlobal1 = state.enableGlobal1 gem.enableGlobal2 = state.enableGlobal2 end @@ -914,6 +932,28 @@ local function applySocketGroupReimportState(socketGroup, state) end end +-- Parses the "tamedBeastProperties" field on rare tamed beast Companion gems into a list of +-- { modId, display, enabled } entries. Each newline-separated line is one rolled monster mod; +-- most lead with a "[ModId|Display]" token, the rest are matched by display line. +function ImportTabClass:ParseTamedBeastProperties(tamedBeastProperties) + local data = self.build.data + local list = { } + for _, property in ipairs(tamedBeastProperties or { }) do + local text = property.values and property.values[1] and property.values[1][1] + if type(text) == "string" then + for line in text:gmatch("[^\n]+") do + local display = escapeGGGString(line) + local modId = line:match("^%[([%w_]+)|[^%]]*%]$") + if not (modId and data.tamedBeastMods[modId]) then + modId = data.tamedBeastModsByDisplay[data.normaliseBeastModLine(display)] + end + t_insert(list, { modId = modId, display = display, enabled = true }) + end + end + end + return list[1] and list or nil +end + function ImportTabClass:ImportItemsAndSkills(charData) local charItemData = charData.equipment if self.controls.charImportItemsClearItems.state then @@ -1034,6 +1074,7 @@ function ImportTabClass:ImportItemsAndSkills(charData) break end end + gemInstance.tamedBeastModList = self:ParseTamedBeastProperties(skillData.tamedBeastProperties) end gemInstance.nameSpec = self.build.data.gems[gemId].name diff --git a/src/Classes/SkillsTab.lua b/src/Classes/SkillsTab.lua index 3c43bfffe3..cbb0fb2537 100644 --- a/src/Classes/SkillsTab.lua +++ b/src/Classes/SkillsTab.lua @@ -282,6 +282,27 @@ will automatically apply to the skill.]] self.controls.gemCorruptHeader = new("LabelControl", {"BOTTOMLEFT", self.gemSlots[1].corruptLevel, "TOPLEFT"}, {0, -2, 0, 16}, "^7Corrupt:") self.controls.gemEnableHeader = new("LabelControl", {"BOTTOMLEFT", self.gemSlots[1].enabled, "TOPLEFT"}, {-16, -2, 0, 16}, "^7Enabled:") self.controls.gemCountHeader = new("LabelControl", {"BOTTOMLEFT", self.gemSlots[1].count, "TOPLEFT"}, {18, -2, 0, 16}, "^7Count:") + + -- Tamed beast (companion) modifiers + self.anchorBeastMods = new("Control", nil, {0, 0, 0, 0}) + self.anchorBeastMods:SetAnchor("TOPLEFT", self.anchorGemSlots, "TOPLEFT", 0, function() + local y = 0 + for i = 1, (self.displayGroup and #self.displayGroup.gemList or 0) + 1 do + local slot = self.gemSlots[i] + y = y + ((slot and (slot.enableGlobal1:IsShown() or slot.enableGlobal2:IsShown())) and 46 or 22) + end + return y + 14 + end) + self.beastModSlots = { } + self:CreateBeastModSlot(1) + self.controls.beastModHeader = new("LabelControl", {"BOTTOMLEFT", self.beastModSlots[1].select, "TOPLEFT"}, {0, -2, 0, 16}, "^7Beast Modifiers:") + self.controls.beastModHeader.shown = function() + return self:GetDisplayedBeastGem() ~= nil + end + self.controls.beastModEnableHeader = new("LabelControl", {"BOTTOMLEFT", self.beastModSlots[1].enabled, "TOPLEFT"}, {-16, -2, 0, 16}, "^7Enabled:") + self.controls.beastModEnableHeader.shown = function() + return self:GetDisplayedBeastGem() ~= nil + end end) function SkillsTabClass:GetCorruptIndex(gemInstance) @@ -386,6 +407,13 @@ function SkillsTabClass:LoadSkill(node, skillSetId) for _, map in ipairs(child) do gemInstance.skillMinionSkillStatSetIndexLookupCalcs[child.attrib.grantedEffect][tonumber(map.attrib.skillIndex)] = tonumber(map.attrib.statSetIndex) end + elseif child.elem == "TamedBeastMod" then + gemInstance.tamedBeastModList = gemInstance.tamedBeastModList or { } + t_insert(gemInstance.tamedBeastModList, { + modId = child.attrib.modId, + display = child.attrib.display, + enabled = child.attrib.enabled == "true", + }) end end @@ -537,6 +565,15 @@ function SkillsTabClass:Save(xml) t_insert(gemInfo, minionSkillStatSetIndexLookupCalcs) end end + if gemInstance.tamedBeastModList then + for _, beastMod in ipairs(gemInstance.tamedBeastModList) do + t_insert(gemInfo, { elem = "TamedBeastMod", attrib = { + modId = beastMod.modId, + display = beastMod.display, + enabled = tostring(beastMod.enabled), + } } ) + end + end t_insert(node, gemInfo) end t_insert(child, node) @@ -603,6 +640,7 @@ function SkillsTabClass:Draw(viewPort, inputEvents) end self:UpdateGemSlots() + self:UpdateBeastModSlots() self:DrawControls(viewPort) end @@ -787,6 +825,13 @@ function SkillsTabClass:CreateGemSlot(index) gemInstance.gemId = gemId gemInstance.skillId = nil self:ProcessSocketGroup(self.displayGroup) + -- Beast mods only exist on Companion gems; clear them when the gem becomes anything else + if gemInstance.tamedBeastModList then + local grantedEffect = gemInstance.gemData and gemInstance.gemData.grantedEffect + if not (grantedEffect and grantedEffect.minionList and grantedEffect.name:match("^Companion")) then + gemInstance.tamedBeastModList = nil + end + end -- New gems need to be constrained by ProcessGemLevel gemInstance.level = self:ProcessGemLevel(gemInstance.gemData) gemInstance.naturalMaxLevel = gemInstance.level @@ -1095,6 +1140,211 @@ function SkillsTabClass:CreateGemSlot(index) self.controls["gemSlot"..index.."EnableGlobal2"] = slot.enableGlobal2 end +-- Returns the displayed group's Companion gem, if any (only companions carry tamed beast mods) +function SkillsTabClass:GetDisplayedBeastGem() + if not self.displayGroup then + return + end + for _, gemInstance in ipairs(self.displayGroup.gemList) do + local grantedEffect = gemInstance.gemData and gemInstance.gemData.grantedEffect + if grantedEffect and grantedEffect.minionList and grantedEffect.name:match("^Companion") then + return gemInstance + end + end +end + +function SkillsTabClass:GetBeastModDropList() + if not self.beastModDropList then + local sorted = { } + local nameCount = { } + for modId, beastMod in pairs(self.build.data.tamedBeastMods) do + -- Non-rollable mods (script-applied, player-minion variants, placeholders) stay + -- resolvable on import but are not offered for selection + if beastMod.rollable then + t_insert(sorted, { modId = modId, beastMod = beastMod }) + nameCount[beastMod.name] = (nameCount[beastMod.name] or 0) + 1 + end + end + table.sort(sorted, function(a, b) + if a.beastMod.name ~= b.beastMod.name then + return a.beastMod.name < b.beastMod.name + end + return (a.beastMod.tier or 0) < (b.beastMod.tier or 0) + end) + local list = { { label = "" } } + for _, entry in ipairs(sorted) do + local label = entry.beastMod.name + if entry.beastMod.tier and nameCount[entry.beastMod.name] > 1 then + label = label .. " (Tier " .. entry.beastMod.tier .. ")" + end + t_insert(list, { label = label, modId = entry.modId, beastMod = entry.beastMod }) + end + self.beastModDropList = list + end + return self.beastModDropList +end + +function SkillsTabClass:CreateBeastModSlot(index) + local slot = { } + self.beastModSlots[index] = slot + + local function getEntry() + local gemInstance = self:GetDisplayedBeastGem() + return gemInstance and gemInstance.tamedBeastModList and gemInstance.tamedBeastModList[index], gemInstance + end + + local function isRowShown() + local gemInstance = self:GetDisplayedBeastGem() + return gemInstance ~= nil and index <= (gemInstance.tamedBeastModList and #gemInstance.tamedBeastModList or 0) + 1 + end + + -- Remove modifier + slot.delete = new("ButtonControl", nil, {0, 0, 20, 20}, "x", function() + local entry, gemInstance = getEntry() + if entry then + t_remove(gemInstance.tamedBeastModList, index) + self:AddUndoState() + self.build.buildFlag = true + end + end) + if index == 1 then + slot.delete:SetAnchor("TOPLEFT", self.anchorBeastMods, "TOPLEFT", 0, 20) + else + slot.delete:SetAnchor("TOPLEFT", self.beastModSlots[index - 1].delete, "BOTTOMLEFT", 0, 2) + end + slot.delete.shown = isRowShown + slot.delete.enabled = function() + return getEntry() ~= nil + end + slot.delete.tooltipText = "Remove this modifier." + self.controls["beastModSlot"..index.."Delete"] = slot.delete + + -- Modifier selection + slot.select = new("DropDownControl", {"LEFT", slot.delete, "RIGHT"}, {2, 0, 300, 20}, self:GetBeastModDropList(), function(indexSel, value) + local entry, gemInstance = getEntry() + if not gemInstance then + return + end + gemInstance.tamedBeastModList = gemInstance.tamedBeastModList or { } + if value.modId then + if entry then + entry.modId = value.modId + entry.display = nil + else + gemInstance.tamedBeastModList[index] = { modId = value.modId, enabled = true } + end + elseif entry then + t_remove(gemInstance.tamedBeastModList, index) + end + self:AddUndoState() + self.build.buildFlag = true + end) + slot.select.shown = isRowShown + slot.select.tooltipFunc = function(tooltip, mode, indexSel, value) + if tooltip:CheckForUpdate(self.build.outputRevision, value, self.displayGroup) then + if mode == "OUT" or not value or not value.beastMod then + return + end + for _, line in ipairs(value.beastMod.statDescriptions) do + if value.beastMod.modList[1] then + tooltip:AddLine(16, colorCodes.MAGIC..line) + else + local line = colorCodes.UNSUPPORTED..line + line = main.notSupportedModTooltips and (line .. main.notSupportedTooltipText) or line + tooltip:AddLine(16, line) + end + end + local entry, gemInstance = getEntry() + if not gemInstance then + return + end + local calcFunc, calcBase = self.build.calcsTab:GetMiscCalculator(self.build) + if calcFunc then + -- Trial-swap the entry for the compare, restoring both the entry and the + -- list reference so a hover can never leave state behind + local storedList = gemInstance.tamedBeastModList + gemInstance.tamedBeastModList = storedList or { } + local storedEntry = gemInstance.tamedBeastModList[index] + gemInstance.tamedBeastModList[index] = { modId = value.modId, enabled = true } + local output = calcFunc() + gemInstance.tamedBeastModList[index] = storedEntry + gemInstance.tamedBeastModList = storedList + tooltip:AddSeparator(10) + self.build:AddStatComparesToTooltip(tooltip, calcBase, output, "^7Selecting this modifier will give you:") + end + end + end + self.controls["beastModSlot"..index.."Select"] = slot.select + + -- Enable modifier + slot.enabled = new("CheckBoxControl", {"LEFT", slot.select, "RIGHT"}, {18, 0, 18}, nil, function(state) + local entry = getEntry() + if entry then + entry.enabled = state + self:AddUndoState() + self.build.buildFlag = true + end + end) + slot.enabled.shown = function() + return getEntry() ~= nil + end + slot.enabled.tooltipFunc = function(tooltip) + if tooltip:CheckForUpdate(self.build.outputRevision, self.displayGroup) then + local entry = getEntry() + if entry and entry.modId then + local calcFunc, calcBase = self.build.calcsTab:GetMiscCalculator(self.build) + if calcFunc then + entry.enabled = not entry.enabled + local output = calcFunc() + entry.enabled = not entry.enabled + self.build:AddStatComparesToTooltip(tooltip, calcBase, output, entry.enabled and "^7Disabling this modifier will give you:" or "^7Enabling this modifier will give you:") + end + end + end + end + self.controls["beastModSlot"..index.."Enable"] = slot.enabled + + -- Imported modifier the dropdown can't represent: unknown id, or known but not in the + -- selectable pool. Either way the entry still applies and can be toggled or removed. + slot.unresolved = new("LabelControl", {"LEFT", slot.enabled, "RIGHT"}, {8, 0, 0, 16}, function() + local entry = getEntry() + if not entry then + return "" + end + if not entry.modId then + return entry.display and ("^1Unrecognised: ^7"..entry.display) or "" + end + local beastMod = self.build.data.tamedBeastMods[entry.modId] + if not (beastMod and beastMod.rollable) then + return "^1Not selectable: ^7"..(beastMod and beastMod.name or entry.modId) + end + return "" + end) + self.controls["beastModSlot"..index.."Unresolved"] = slot.unresolved +end + +-- Update the beast mod slot controls to reflect the currently displayed socket group's companion +function SkillsTabClass:UpdateBeastModSlots() + local gemInstance = self:GetDisplayedBeastGem() + if not gemInstance then + return + end + -- Create slots on demand, like gem slots: one row per entry plus an empty row to add more + for index = 1, (gemInstance.tamedBeastModList and #gemInstance.tamedBeastModList or 0) + 1 do + if not self.beastModSlots[index] then + self:CreateBeastModSlot(index) + end + end + for index, slot in ipairs(self.beastModSlots) do + local entry = gemInstance.tamedBeastModList and gemInstance.tamedBeastModList[index] + slot.select.selIndex = 1 + if entry and entry.modId then + slot.select:SelByValue(entry.modId, "modId") + end + slot.enabled.state = entry and entry.enabled or false + end +end + -- Update the gem slot controls to reflect the currently displayed socket group function SkillsTabClass:UpdateGemSlots() if not self.displayGroup then diff --git a/src/Data/TamedBeastMods.lua b/src/Data/TamedBeastMods.lua new file mode 100644 index 0000000000..8448f01f6b --- /dev/null +++ b/src/Data/TamedBeastMods.lua @@ -0,0 +1,2180 @@ +-- This file is automatically generated, do not edit! +-- Path of Building +-- +-- Tamed Beast (Companion) Modifier Data +-- Monster data (c) Grinding Gear Games + +local mods, mod, flag = ... + +mods["MonsterDamageGainedAsFire1"] = { + name = "Extra Fire Damage", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + "Monster Gains 40% of damage as extra Fire damage.", + }, + modList = { + mod("DamageGainAsFire", "BASE", 40, 0, 0), -- MonsterDamageGainedAsFire1 [non_skill_base_all_damage_%_to_gain_as_fire = 40] + }, +} + +mods["PlayerMonsterDamageGainedAsFire1"] = { + name = "Extra Fire Damage", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("DamageGainAsFire", "BASE", 40, 0, 0), -- PlayerMonsterDamageGainedAsFire1 [non_skill_base_all_damage_%_to_gain_as_fire = 40] + }, +} + +mods["MonsterDamageGainedAsCold1"] = { + name = "Extra Cold Damage", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + "Monster Gains 40% of damage as extra Cold damage.", + }, + modList = { + mod("DamageGainAsCold", "BASE", 40, 0, 0), -- MonsterDamageGainedAsCold1 [non_skill_base_all_damage_%_to_gain_as_cold = 40] + }, +} + +mods["PlayerMonsterDamageGainedAsCold1"] = { + name = "Extra Cold Damage", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("DamageGainAsCold", "BASE", 40, 0, 0), -- PlayerMonsterDamageGainedAsCold1 [non_skill_base_all_damage_%_to_gain_as_cold = 40] + }, +} + +mods["MonsterDamageGainedAsLightning1"] = { + name = "Extra Lightning Damage", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + "Monster Gains 40% of damage as extra Lightning damage.", + }, + modList = { + mod("DamageGainAsLightning", "BASE", 40, 0, 0), -- MonsterDamageGainedAsLightning1 [non_skill_base_all_damage_%_to_gain_as_lightning = 40] + }, +} + +mods["PlayerMonsterDamageGainedAsLightning1"] = { + name = "Extra Lightning Damage", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("DamageGainAsLightning", "BASE", 40, 0, 0), -- PlayerMonsterDamageGainedAsLightning1 [non_skill_base_all_damage_%_to_gain_as_lightning = 40] + }, +} + +mods["MonsterIncreasedSpeed1"] = { + name = "Hasted", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + "Monster has 30% increased Attack, Cast and Movement speed.", + }, + modList = { + mod("Speed", "INC", 30, 0, 0), -- MonsterIncreasedSpeed1 [attack_and_cast_speed_+% = 30] + mod("MovementSpeed", "INC", 30, 0, 0), -- MonsterIncreasedSpeed1 [base_movement_velocity_+% = 30] + }, +} + +mods["PlayerMonsterIncreasedSpeed1"] = { + name = "Hasted", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("Speed", "INC", 30, 0, 0), -- PlayerMonsterIncreasedSpeed1 [attack_and_cast_speed_+% = 30] + mod("MovementSpeed", "INC", 30, 0, 0), -- PlayerMonsterIncreasedSpeed1 [base_movement_velocity_+% = 30] + }, +} + +mods["MonsterCriticalStrikeChance1"] = { + name = "Extra Crits", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + "Monster has 300% increased chance to Critically Hit.", + }, + modList = { + mod("CritChance", "INC", 300, 0, 0), -- MonsterCriticalStrikeChance1 [critical_strike_chance_+% = 300] + }, +} + +mods["PlayerMonsterCriticalStrikeChance1"] = { + name = "Extra Crits", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("CritChance", "INC", 300, 0, 0), -- PlayerMonsterCriticalStrikeChance1 [critical_strike_chance_+% = 300] + }, +} + +mods["MonsterStunDamageIncrease1"] = { + name = "Stuns", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + "Monster has 100% increased Stun buildup.", + }, + modList = { + -- MonsterStunDamageIncrease1 [hit_damage_stun_multiplier_+% = 100] + }, +} + +mods["PlayerMonsterStunDamageIncrease1"] = { + name = "Stuns", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + -- PlayerMonsterStunDamageIncrease1 [hit_damage_stun_multiplier_+% = 100] + }, +} + +mods["MonsterExtraArmour1"] = { + name = "Armoured", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + "Monster gains extra Armour based off of their Strength.", + }, + modList = { + -- MonsterExtraArmour1 [monster_additional_strength_ratio_%_for_armour = 100] + }, +} + +mods["PlayerMonsterExtraArmour1"] = { + name = "Armoured", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + -- PlayerMonsterExtraArmour1 [monster_additional_strength_ratio_%_for_armour = 100] + }, +} + +mods["MonsterExtraEvasion1"] = { + name = "Evasive", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + "Monster gains extra Evasion based off of their Dexterity.", + }, + modList = { + -- MonsterExtraEvasion1 [monster_additional_dexterity_ratio_%_for_evasion = 100] + }, +} + +mods["PlayerMonsterExtraEvasion1"] = { + name = "Evasive", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + -- PlayerMonsterExtraEvasion1 [monster_additional_dexterity_ratio_%_for_evasion = 100] + }, +} + +mods["MonsterExtraEnergyShield1"] = { + name = "Extra Energy Shield", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + "Monster gains 25% of Maximum life as added Energy Shield.", + }, + modList = { + -- MonsterExtraEnergyShield1 [base_maximum_life_%_to_gain_as_total_energy_shield = 25] + }, +} + +mods["PlayerMonsterExtraEnergyShield1"] = { + name = "Extra Energy Shield", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + -- PlayerMonsterExtraEnergyShield1 [base_maximum_life_%_to_gain_as_total_energy_shield = 25] + }, +} + +mods["MonsterAlwaysPoison1"] = { + name = "Always Poisons", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("PoisonChance", "BASE", 100, 0, 0), -- MonsterAlwaysPoison1 [global_poison_on_hit = 1] + }, +} + +mods["PlayerMonsterAlwaysPoison1"] = { + name = "Always Poisons", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("PoisonChance", "BASE", 100, 0, 0), -- PlayerMonsterAlwaysPoison1 [global_poison_on_hit = 1] + }, +} + +mods["MonsterAlwaysBleed1"] = { + name = "Always Bleeds", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("BleedChance", "BASE", 100, 0, 0), -- MonsterAlwaysBleed1 [global_bleed_on_hit = 1] + }, +} + +mods["PlayerMonsterAlwaysBleed1"] = { + name = "Always Bleeds", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("BleedChance", "BASE", 100, 0, 0), -- PlayerMonsterAlwaysBleed1 [global_bleed_on_hit = 1] + }, +} + +mods["MonsterBurningGroundOnDeath1"] = { + name = "Periodically unleashes Fire", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["PlayerMonsterBurningGroundOnDeath1"] = { + name = "Burning Ground on Death", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterChilledGroundOnDeath1"] = { + name = "Periodically unleashes Ice", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["PlayerMonsterChilledGroundOnDeath1"] = { + name = "Periodically unleashes Ice", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterShockedGroundOnDeath1"] = { + name = "Periodically unleashes Lightning", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["PlayerMonsterShockedGroundOnDeath1"] = { + name = "Periodically unleashes Lightning", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterImmuneToStun1"] = { + name = "Increased Stun Threshold", + type = "Suffix", + tier = 1, + statDescriptions = { + "Monster cannot be Stunned.", + }, + modList = { + mod("StunImmune", "FLAG", 1, 0, 0), -- MonsterImmuneToStun1 [base_cannot_be_stunned = 1] + }, +} + +mods["PlayerMonsterImmuneToStun1"] = { + name = "Increased Stun Threshold", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("StunImmune", "FLAG", 1, 0, 0), -- PlayerMonsterImmuneToStun1 [base_cannot_be_stunned = 1] + }, +} + +mods["MonsterStunResilience1"] = { + name = "Stun Resistant", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + "Monster has 250% increased Stun Threshold.", + }, + modList = { + mod("StunThreshold", "INC", 250, 0, 0), -- MonsterStunResilience1 [stun_threshold_+% = 250] + }, +} + +mods["PlayerMonsterStunResilience1"] = { + name = "Stun Resistant", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("StunThreshold", "INC", 250, 0, 0), -- PlayerMonsterStunResilience1 [stun_threshold_+% = 250] + }, +} + +mods["MonsterFireResistance1"] = { + name = "Fire Resistant", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + "Monster has +50% to Fire Resistance and +10% to Maximum Fire Resistance.", + }, + modList = { + mod("FireResist", "BASE", 50, 0, 0), -- MonsterFireResistance1 [base_fire_damage_resistance_% = 50] + mod("FireResistMax", "BASE", 10, 0, 0), -- MonsterFireResistance1 [base_maximum_fire_damage_resistance_% = 10] + }, +} + +mods["PlayerMonsterFireResistance1"] = { + name = "Fire Resistant", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("FireResist", "BASE", 50, 0, 0), -- PlayerMonsterFireResistance1 [base_fire_damage_resistance_% = 50] + mod("FireResistMax", "BASE", 10, 0, 0), -- PlayerMonsterFireResistance1 [base_maximum_fire_damage_resistance_% = 10] + }, +} + +mods["MonsterColdResistance1"] = { + name = "Cold Resistant", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + "Monster has +50% to Cold Resistance and +10% to Maximum Cold Resistance.", + }, + modList = { + mod("ColdResist", "BASE", 50, 0, 0), -- MonsterColdResistance1 [base_cold_damage_resistance_% = 50] + mod("ColdResistMax", "BASE", 10, 0, 0), -- MonsterColdResistance1 [base_maximum_cold_damage_resistance_% = 10] + }, +} + +mods["PlayerMonsterColdResistance1"] = { + name = "Cold Resistant", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("ColdResist", "BASE", 50, 0, 0), -- PlayerMonsterColdResistance1 [base_cold_damage_resistance_% = 50] + mod("ColdResistMax", "BASE", 10, 0, 0), -- PlayerMonsterColdResistance1 [base_maximum_cold_damage_resistance_% = 10] + }, +} + +mods["MonsterLightningResistance1"] = { + name = "Lightning Resistant", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + "Monster has +50% to Lightning Resistance and +10% to Maximum Lightning Resistance.", + }, + modList = { + mod("LightningResist", "BASE", 50, 0, 0), -- MonsterLightningResistance1 [base_lightning_damage_resistance_% = 50] + mod("LightningResistMax", "BASE", 10, 0, 0), -- MonsterLightningResistance1 [base_maximum_lightning_damage_resistance_% = 10] + }, +} + +mods["PlayerMonsterLightningResistance1"] = { + name = "Lightning Resistant", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("LightningResist", "BASE", 50, 0, 0), -- PlayerMonsterLightningResistance1 [base_lightning_damage_resistance_% = 50] + mod("LightningResistMax", "BASE", 10, 0, 0), -- PlayerMonsterLightningResistance1 [base_maximum_lightning_damage_resistance_% = 10] + }, +} + +mods["MonsterArmourPenetration1"] = { + name = "Breaks Armour", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + "Monster Breaks Armour equal to 1000% of Physical Damage dealt.", + }, + modList = { + mod("Condition:CanArmourBreak", "FLAG", 1000, 0, 0, { effectName = "ArmourBreak", effectType = "Buff", type = "GlobalEffect" }), -- MonsterArmourPenetration1 [armour_break_physical_damage_%_dealt_as_armour_break = 1000] + }, +} + +mods["PlayerMonsterArmourPenetration1"] = { + name = "Breaks Armour", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("Condition:CanArmourBreak", "FLAG", 1000, 0, 0, { effectName = "ArmourBreak", effectType = "Buff", type = "GlobalEffect" }), -- PlayerMonsterArmourPenetration1 [armour_break_physical_damage_%_dealt_as_armour_break = 1000] + }, +} + +mods["MonsterIncreasedAccuracy1"] = { + name = "Accurate", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + "Monster has 200% increased Accuracy Rating.", + }, + modList = { + mod("Accuracy", "INC", 200, 0, 0), -- MonsterIncreasedAccuracy1 [accuracy_rating_+% = 200] + }, +} + +mods["PlayerMonsterIncreasedAccuracy1"] = { + name = "Accurate", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("Accuracy", "INC", 200, 0, 0), -- PlayerMonsterIncreasedAccuracy1 [accuracy_rating_+% = 200] + }, +} + +mods["MonsterDamageGainedAsChaos1"] = { + name = "Extra Chaos Damage", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + "Monster Gains 40% of damage as extra Chaos damage.", + }, + modList = { + mod("DamageGainAsChaos", "BASE", 40, 0, 0), -- MonsterDamageGainedAsChaos1 [non_skill_base_all_damage_%_to_gain_as_chaos = 40] + }, +} + +mods["PlayerMonsterDamageGainedAsChaos1"] = { + name = "Extra Chaos Damage", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("DamageGainAsChaos", "BASE", 40, 0, 0), -- PlayerMonsterDamageGainedAsChaos1 [non_skill_base_all_damage_%_to_gain_as_chaos = 40] + }, +} + +mods["MonsterLifeRegenerationRatePercentage1"] = { + name = "Regenerates Life", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + "Monster Regenerates 2% of Maximum Life per second.", + }, + modList = { + mod("LifeRegenPercent", "BASE", 2, 0, 0), -- MonsterLifeRegenerationRatePercentage1 [life_regeneration_rate_per_minute_% = 120] + }, +} + +mods["PlayerMonsterLifeRegenerationRatePercentage1"] = { + name = "Regenerates Life", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("LifeRegenPercent", "BASE", 2, 0, 0), -- PlayerMonsterLifeRegenerationRatePercentage1 [life_regeneration_rate_per_minute_% = 120] + }, +} + +mods["MonsterAdditionalProjectiles1"] = { + name = "Additional Projectiles", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + "Monster fires 4 additional Projectiles.", + }, + modList = { + mod("ProjectileCount", "BASE", 4, 0, 0), -- MonsterAdditionalProjectiles1 [number_of_additional_projectiles = 4] + }, +} + +mods["PlayerMonsterAdditionalProjectiles1"] = { + name = "Additional Projectiles", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("ProjectileCount", "BASE", 4, 0, 0), -- PlayerMonsterAdditionalProjectiles1 [number_of_additional_projectiles = 4] + }, +} + +mods["MonsterAreaOfEffect1"] = { + name = "Increased Area of Effect", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + "Monster has 100% Increased Area of Effect.", + "100% more Area of Effect", + }, + modList = { + -- MonsterAreaOfEffect1 [rare_monster_mod_area_of_effect_+%_final = 100] + }, +} + +mods["PlayerMonsterAreaOfEffect1"] = { + name = "Increased Area of Effect", + type = "Suffix", + tier = 1, + statDescriptions = { + "100% more Area of Effect", + }, + modList = { + -- PlayerMonsterAreaOfEffect1 [rare_monster_mod_area_of_effect_+%_final = 100] + }, +} + +mods["MonsterIgniteChanceIncrease1"] = { + name = "All Damage Ignites", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("PhysicalCanIgnite", "FLAG", 1, 0, 0), -- MonsterIgniteChanceIncrease1 [all_damage_can_ignite = 1] + mod("EnemyIgniteChance", "BASE", 100, 0, 0), -- MonsterIgniteChanceIncrease1 [always_ignite = 1] + }, +} + +mods["PlayerMonsterIgniteChanceIncrease1"] = { + name = "All Damage Ignites", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("PhysicalCanIgnite", "FLAG", 1, 0, 0), -- PlayerMonsterIgniteChanceIncrease1 [all_damage_can_ignite = 1] + mod("EnemyIgniteChance", "BASE", 100, 0, 0), -- PlayerMonsterIgniteChanceIncrease1 [always_ignite = 1] + }, +} + +mods["MonsterFreezeDamageIncrease1"] = { + name = "All Damage Chills", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + -- MonsterFreezeDamageIncrease1 [all_damage_can_chill = 1] + -- MonsterFreezeDamageIncrease1 [chill_minimum_slow_% = 10] + }, +} + +mods["PlayerMonsterFreezeDamageIncrease1"] = { + name = "All Damage Chills", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + -- PlayerMonsterFreezeDamageIncrease1 [all_damage_can_chill = 1] + -- PlayerMonsterFreezeDamageIncrease1 [chill_minimum_slow_% = 10] + }, +} + +mods["MonsterShockChanceIncrease1"] = { + name = "All Damage Shocks", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("PhysicalCanShock", "FLAG", 1, 0, 0), -- MonsterShockChanceIncrease1 [all_damage_can_shock = 1] + mod("EnemyShockChance", "BASE", 100, 0, 0), -- MonsterShockChanceIncrease1 [always_shock = 1] + }, +} + +mods["PlayerMonsterShockChanceIncrease1"] = { + name = "All Damage Shocks", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("PhysicalCanShock", "FLAG", 1, 0, 0), -- PlayerMonsterShockChanceIncrease1 [all_damage_can_shock = 1] + mod("EnemyShockChance", "BASE", 100, 0, 0), -- PlayerMonsterShockChanceIncrease1 [always_shock = 1] + }, +} + +mods["MonsterBurningGroundTrail1"] = { + name = "Trail of Fire", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["PlayerMonsterBurningGroundTrail1"] = { + name = "Trail of Fire", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterChilledGroundTrail1"] = { + name = "Trail of Ice", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["PlayerMonsterChilledGroundTrail1"] = { + name = "Trail of Ice", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterShockedGroundTrail1"] = { + name = "Trail of Lightning", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + "Monster leaves a trail of Shocked Ground as they move.", + }, + modList = { + }, +} + +mods["PlayerMonsterShockedGroundTrail1"] = { + name = "Trail of Lightning", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterImmuneToSlow1"] = { + name = "Slow Resistant", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + "Monster has 50% reduced Slowing Potency of Debuffs on them.", + "50% less Slowing Potency of Debuffs on me", + }, + modList = { + -- MonsterImmuneToSlow1 [monster_slow_potency_+%_final = -50] + }, +} + +mods["PlayerMonsterImmuneToSlow1"] = { + name = "Slow Resistant", + type = "Suffix", + tier = 1, + statDescriptions = { + "50% less Slowing Potency of Debuffs on me", + }, + modList = { + -- PlayerMonsterImmuneToSlow1 [monster_slow_potency_+%_final = -50] + }, +} + +mods["MonsterChaosResistance1"] = { + name = "Chaos Resistant", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + "Monster has +50% to Chaos Resistance.", + }, + modList = { + mod("ChaosResist", "BASE", 50, 0, 0), -- MonsterChaosResistance1 [base_chaos_damage_resistance_% = 50] + }, +} + +mods["PlayerMonsterChaosResistance1"] = { + name = "Chaos Resistant", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("ChaosResist", "BASE", 50, 0, 0), -- PlayerMonsterChaosResistance1 [base_chaos_damage_resistance_% = 50] + }, +} + +mods["MonsterCannotLeech1"] = { + name = "of Congealment", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + -- MonsterCannotLeech1 [life_leeched_from_-permyriad = 6000] + -- MonsterCannotLeech1 [mana_leeched_from_-permyriad = 4000] + -- MonsterCannotLeech1 [energy_shield_leeched_from_-permyriad = 6000] + }, +} + +mods["PlayerMonsterCannotLeech1"] = { + name = "of Congealment", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + -- PlayerMonsterCannotLeech1 [life_leeched_from_-permyriad = 6000] + -- PlayerMonsterCannotLeech1 [mana_leeched_from_-permyriad = 4000] + -- PlayerMonsterCannotLeech1 [energy_shield_leeched_from_-permyriad = 6000] + }, +} + +mods["MonsterIsHexproof1"] = { + name = "of Hexproof", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + "Hexproof", + }, + modList = { + -- MonsterIsHexproof1 [hexproof = 1] + }, +} + +mods["PlayerMonsterIsHexproof1"] = { + name = "of Hexproof", + type = "Suffix", + tier = 1, + statDescriptions = { + "Hexproof", + }, + modList = { + -- PlayerMonsterIsHexproof1 [hexproof = 1] + }, +} + +mods["MonsterAdditionalChains1"] = { + name = "of Chaining", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("ChainCountMax", "BASE", 2, 0, 0), -- MonsterAdditionalChains1 [number_of_chains = 2] + -- MonsterAdditionalChains1 [projectile_chain_from_terrain_chance_% = 50] + }, +} + +mods["PlayerMonsterAdditionalChains1"] = { + name = "of Chaining", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("ChainCountMax", "BASE", 2, 0, 0), -- PlayerMonsterAdditionalChains1 [number_of_chains = 2] + -- PlayerMonsterAdditionalChains1 [projectile_chain_from_terrain_chance_% = 50] + }, +} + +mods["MonsterProjectilesGainDamage1"] = { + name = "of Far Shot", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + -- MonsterProjectilesGainDamage1 [projectile_damage_+%_max_as_distance_travelled_increases = 60] + }, +} + +mods["PlayerMonsterProjectilesGainDamage1"] = { + name = "of Far Shot", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + -- PlayerMonsterProjectilesGainDamage1 [projectile_damage_+%_max_as_distance_travelled_increases = 60] + }, +} + +mods["MonsterModReducedCritMulti1"] = { + name = "Crit Resistant", + rollable = true, + type = "Suffix", + tier = 1, + statDescriptions = { + "Hits against this Monster have 80% reduced Critical Damage Bonus.", + }, + modList = { + mod("SelfCritMultiplier", "INC", -80, 0, 0), -- MonsterModReducedCritMulti1 [base_self_critical_strike_multiplier_-% = 80] + }, +} + +mods["PlayerMonsterModReducedCritMulti1"] = { + name = "Crit Resistant", + type = "Suffix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("SelfCritMultiplier", "INC", -80, 0, 0), -- PlayerMonsterModReducedCritMulti1 [base_self_critical_strike_multiplier_-% = 80] + }, +} + +mods["MonsterLastGasp1"] = { + name = "TBD", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + -- MonsterLastGasp1 [retaliation_godmode_ghost_duration_ms = 10000] + -- MonsterLastGasp1 [corpse_cannot_be_destroyed = 1] + -- MonsterLastGasp1 [cannot_be_dominated = 1] + }, +} + +mods["PlayerMonsterLastGasp1"] = { + name = "TBD", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + -- PlayerMonsterLastGasp1 [retaliation_godmode_ghost_duration_ms = 10000] + -- PlayerMonsterLastGasp1 [corpse_cannot_be_destroyed = 1] + -- PlayerMonsterLastGasp1 [cannot_be_dominated = 1] + }, +} + +mods["MonsterFlameBeacons1"] = { + name = "Periodic Fire Explosions", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["PlayerMonsterFlameBeacons1"] = { + name = "Periodic Fire Explosions", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterFrostBeacons1"] = { + name = "Periodic Cold Explosions", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["PlayerMonsterFrostBeacons1"] = { + name = "Periodic Cold Explosions", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterLightningBeacons1"] = { + name = "Periodic Lightning Explosions", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["PlayerMonsterLightningBeacons1"] = { + name = "Periodic Lightning Explosions", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterStrongerMinions1"] = { + name = "Powerful Minions", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + "Monster's Pack Minions have 25% increased Damage and 50% increased Life.", + }, + modList = { + }, +} + +mods["PlayerMonsterStrongerMinions1"] = { + name = "Powerful Minions", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterPhysicalDamageAura1"] = { + name = "Extra Physical Damage Aura", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + "Monster creates an Aura that grants 40% increased Physical Damage to Allies within 5 metres.", + }, + modList = { + mod("PhysicalDamage", "INC", 40, 0, 0), -- MonsterPhysicalDamageAura1 [physical_damage_+% = 40] + }, +} + +mods["PlayerMonsterPhysicalDamageAura1"] = { + name = "Extra Physical Damage Aura", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("PhysicalDamage", "INC", 40, 0, 0), -- PlayerMonsterPhysicalDamageAura1 [physical_damage_+% = 40] + }, +} + +mods["MonsterIncreasedSpeedAura1"] = { + name = "Haste Aura", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + "Monster creates an Aura that grants 20% increased Attack and Cast speed and 10% increased Movement speed to Allies within 5 metres.", + }, + modList = { + mod("Speed", "INC", 25, 0, 0), -- MonsterIncreasedSpeedAura1 [attack_and_cast_speed_+% = 25] + mod("MovementSpeed", "INC", 25, 0, 0), -- MonsterIncreasedSpeedAura1 [base_movement_velocity_+% = 25] + }, +} + +mods["PlayerMonsterIncreasedSpeedAura1"] = { + name = "Haste Aura", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("Speed", "INC", 25, 0, 0), -- PlayerMonsterIncreasedSpeedAura1 [attack_and_cast_speed_+% = 25] + mod("MovementSpeed", "INC", 25, 0, 0), -- PlayerMonsterIncreasedSpeedAura1 [base_movement_velocity_+% = 25] + }, +} + +mods["PlayerMonsterIncreasedSpeedAuraMinion1"] = { + name = "Haste Aura", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("Speed", "INC", 20, 0, 0), -- PlayerMonsterIncreasedSpeedAuraMinion1 [attack_and_cast_speed_+% = 20] + mod("MovementSpeed", "INC", 10, 0, 0), -- PlayerMonsterIncreasedSpeedAuraMinion1 [base_movement_velocity_+% = 10] + }, +} + +mods["MonsterEnergyShieldAura1"] = { + name = "Energy Shield Aura", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + "Monster creates an Aura that grants 20% of Maximum life as added Energy Shield to Allies within 5 metres.", + }, + modList = { + -- MonsterEnergyShieldAura1 [base_maximum_life_%_to_gain_as_total_energy_shield = 30] + }, +} + +mods["PlayerMonsterEnergyShieldAura1"] = { + name = "Energy Shield Aura", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + -- PlayerMonsterEnergyShieldAura1 [base_maximum_life_%_to_gain_as_total_energy_shield = 30] + }, +} + +mods["PlayerMonsterEnergyShieldAuraMinion1"] = { + name = "Energy Shield Aura", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + -- PlayerMonsterEnergyShieldAuraMinion1 [base_maximum_life_%_to_gain_as_total_energy_shield = 20] + }, +} + +mods["MonsterResistanceAura1"] = { + name = "Elemental Resistance Aura", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + "Monster creates an Aura that grants +35% to all Elemental Resistances to Allies within 5 metres.", + }, + modList = { + mod("ElementalResist", "BASE", 35, 0, 0), -- MonsterResistanceAura1 [base_resist_all_elements_% = 35] + }, +} + +mods["PlayerMonsterResistanceAura1"] = { + name = "Elemental Resistance Aura", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + mod("ElementalResist", "BASE", 35, 0, 0), -- PlayerMonsterResistanceAura1 [base_resist_all_elements_% = 35] + }, +} + +mods["MonsterTemporalAura1"] = { + name = "Temporal Bubble", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + "Monster creates an Aura that Debuffs Enemies within 3.2 metres; Slowing by 25%, making effects expire 40% slower and reducing Cooldown Recovery Rate by 60%.", + }, + modList = { + -- MonsterTemporalAura1 [action_speed_-% = 25] + -- MonsterTemporalAura1 [debuff_time_passed_+% = -40] + mod("CooldownRecovery", "INC", -60, 0, 0), -- MonsterTemporalAura1 [base_cooldown_speed_+% = -60] + -- MonsterTemporalAura1 [cannot_be_damaged_by_things_outside_radius = 0] + }, +} + +mods["PlayerMonsterTemporalAura1"] = { + name = "Temporal Bubble", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + -- PlayerMonsterTemporalAura1 [action_speed_-% = 25] + -- PlayerMonsterTemporalAura1 [debuff_time_passed_+% = -40] + mod("CooldownRecovery", "INC", -60, 0, 0), -- PlayerMonsterTemporalAura1 [base_cooldown_speed_+% = -60] + -- PlayerMonsterTemporalAura1 [cannot_be_damaged_by_things_outside_radius = 0] + }, +} + +mods["PlayerMonsterTemporalAuraMinion1"] = { + name = "Temporal Bubble", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + -- PlayerMonsterTemporalAuraMinion1 [action_speed_-% = 12] + -- PlayerMonsterTemporalAuraMinion1 [debuff_time_passed_+% = -40] + mod("CooldownRecovery", "INC", -20, 0, 0), -- PlayerMonsterTemporalAuraMinion1 [base_cooldown_speed_+% = -20] + -- PlayerMonsterTemporalAuraMinion1 [cannot_be_damaged_by_things_outside_radius = 0] + }, +} + +mods["MonsterHinderAura1"] = { + name = "Hinder Aura", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + "Monster creates an Aura that Hinders enemies within 3.6 metres.", + }, + modList = { + }, +} + +mods["PlayerMonsterHinderAura1"] = { + name = "Hinder Aura", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterPreventRecoveryAura1"] = { + name = "Prevents Recovery Above 50%", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + "Monster creates an Aura that Debuffs enemies within 4.2 metres, causing their Life and Energy Shield to not be able to recover past 50%.", + }, + modList = { + -- MonsterPreventRecoveryAura1 [cannot_recover_life_or_energy_shield_above_% = 50] + }, +} + +mods["PlayerMonsterPreventRecoveryAura1"] = { + name = "Prevents Recovery Above 50%", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterImmuneAura1"] = { + name = "Periodic Invulnerability Aura", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + "Monster releases a nova that makes Allies invulnerable for 5 seconds while the monster is alive within 5 metres every 12 seconds.", + }, + modList = { + -- MonsterImmuneAura1 [monster_allies_cannot_take_damage_pulse_owner = 1] + }, +} + +mods["PlayerMonsterImmuneAura1"] = { + name = "Periodic Invulnerability Aura", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + -- PlayerMonsterImmuneAura1 [monster_allies_cannot_take_damage_pulse_owner = 1] + }, +} + +mods["MonsterImmuneAura2"] = { + name = "Empowered Periodic Invulnerability Aura", + rollable = true, + type = "Prefix", + tier = 2, + statDescriptions = { + "Monster releases a nova that makes Allies invulnerable for 5 seconds while the monster is alive within 5 metres every 12 seconds.", + }, + modList = { + -- MonsterImmuneAura2 [monster_allies_cannot_take_damage_pulse_owner = 1] + }, +} + +mods["PlayerMonsterImmuneAura2"] = { + name = "Empowered Periodic Invulnerability Aura", + type = "Prefix", + tier = 2, + statDescriptions = { + }, + modList = { + -- PlayerMonsterImmuneAura2 [monster_allies_cannot_take_damage_pulse_owner = 1] + }, +} + +mods["MonsterImmuneAuraMinion2"] = { + name = "Increased Life", + type = "Prefix", + tier = 2, + statDescriptions = { + "Monster has 50% increased Life.", + }, + modList = { + -- MonsterImmuneAuraMinion2 [base_maximum_life = 0] + -- MonsterImmuneAuraMinion2 [maximum_life_+% = 50] + }, +} + +mods["MonsterManaSiphonAura1"] = { + name = "Siphons Mana and Deals Lightning Damage", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + "Monster creates a circular effect that drains Mana and deals Lightning Damage over time to enemies near the edge of the circle.", + }, + modList = { + }, +} + +mods["PlayerMonsterManaSiphonAura1"] = { + name = "Siphons Mana and Deals Lightning Damage", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterManaSiphonAura2"] = { + name = "Siphons Mana and Deals Lightning Damage", + rollable = true, + type = "Prefix", + tier = 2, + statDescriptions = { + "Monster creates a circular effect that drains Mana and deals Lightning Damage over time to enemies near the edge of the circle. Additionally, Monster will periodically create separate circles that drain Mana and deal Lightning Damage over time to enemies standing in them.", + }, + modList = { + }, +} + +mods["PlayerMonsterManaSiphonAura2"] = { + name = "Siphons Mana and Deals Lightning Damage", + type = "Prefix", + tier = 2, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterHealingNova1"] = { + name = "Heals Allies and Suppresses Foe Recovery", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + "Monster releases a nova that reduces Enemy Life and Energy Shield Recovery Rate by 60% and causes Allies to Regenerate 5.5% of Maximum Life per second for 4 seconds within 5 metres every 8 seconds.", + }, + modList = { + }, +} + +mods["PlayerMonsterHealingNova1"] = { + name = "Heals Allies and Suppresses Foe Recovery", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterFlaskRemovalAura1"] = { + name = "Siphons Flask Charges", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + "Monster creates an Aura that removes 3 Flask and Charm charges from enemies every 3 seconds within 3.6 metres.", + }, + modList = { + -- MonsterFlaskRemovalAura1 [generate_x_charges_for_any_flask_per_minute = -3] + }, +} + +mods["PlayerMonsterFlaskRemovalAura1"] = { + name = "Siphons Flask Charges", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + -- PlayerMonsterFlaskRemovalAura1 [generate_x_charges_for_any_flask_per_minute = -3] + }, +} + +mods["MonsterRevivesMinions1"] = { + name = "Reviving Minions", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + "Monster periodically revives Pack Minions.", + }, + modList = { + }, +} + +mods["PlayerMonsterRevivesMinions1"] = { + name = "Reviving Minions", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterRevivesMinions2"] = { + name = "Empowered Reviving Minions", + rollable = true, + type = "Prefix", + tier = 2, + statDescriptions = { + "Monster periodically revives Pack Minions with increased Life and Damage.", + }, + modList = { + }, +} + +mods["PlayerMonsterRevivesMinions2"] = { + name = "Empowered Reviving Minions", + type = "Prefix", + tier = 2, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterMinionsTakeLifeInstead1"] = { + name = "Damage Taken From Minions First", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + "50% of damage taken from Monster is taken from Monster's Pack Minions instead", + }, + modList = { + -- MonsterMinionsTakeLifeInstead1 [damage_removed_from_pack_minions_before_life_or_es_% = 50] + }, +} + +mods["PlayerMonsterMinionsTakeLifeInstead1"] = { + name = "Damage Taken From Minions First", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + -- PlayerMonsterMinionsTakeLifeInstead1 [damage_removed_from_pack_minions_before_life_or_es_% = 50] + }, +} + +mods["MonsterShroudWalker1"] = { + name = "Shroud Walker", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + "Monster periodically teleports to an enemy they can see, creating a Smoke Cloud where they leave and where they teleport to.", + }, + modList = { + }, +} + +mods["PlayerMonsterShroudWalker1"] = { + name = "Shroud Walker", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterShroudWalker2"] = { + name = "Shroud Walker", + rollable = true, + type = "Prefix", + tier = 2, + statDescriptions = { + "Monster periodically teleports to an enemy they can see, creating a Smoke Cloud where they leave and where they teleport to.", + }, + modList = { + }, +} + +mods["PlayerMonsterShroudWalker2"] = { + name = "Shroud Walker", + type = "Prefix", + tier = 2, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterPeriodicEnrage1"] = { + name = "Periodically Enrages", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + "Monster periodically Enrages; gaining 30% increased Damage, 25% increased Skill and Movement Speed and 33% less damage taken for 5 seconds every 10 seconds.", + }, + modList = { + }, +} + +mods["PlayerMonsterPeriodicEnrage1"] = { + name = "Periodically Enrages", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterPeriodicEnrage2"] = { + name = "Enraged", + rollable = true, + type = "Prefix", + tier = 2, + statDescriptions = { + "Monster is Enraged; gaining 30% increased Damage, 25% increased Skill and Movement Speed and 33% less damage taken.", + }, + modList = { + }, +} + +mods["PlayerMonsterPeriodicEnrage2"] = { + name = "Enraged", + type = "Prefix", + tier = 2, + statDescriptions = { + }, + modList = { + }, +} + +mods["PlayerMonsterPeriodicEnrageMinion2"] = { + name = "Enraged", + type = "Prefix", + tier = 2, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterCorpseExploder1"] = { + name = "Explodes Nearby Corpses", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["PlayerMonsterCorpseExploder1"] = { + name = "Explodes Nearby Corpses", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterLightningMirage1"] = { + name = "Lightning Mirage When Hit", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + "Monster creates a Mirage when Hit that moves towards enemies and explodes when it gets close enough, dealing Lightning Damage.", + }, + modList = { + }, +} + +mods["PlayerMonsterLightningMirage1"] = { + name = "Lightning Mirage When Hit", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterLightningMirage2"] = { + name = "Lightning Mirages When Hit", + rollable = true, + type = "Prefix", + tier = 2, + statDescriptions = { + "Monster creates Mirages when Hit that move towards enemies and explode when they get close enough, dealing Lightning Damage.", + }, + modList = { + }, +} + +mods["PlayerMonsterLightningMirage2"] = { + name = "Lightning Mirages When Hit", + type = "Prefix", + tier = 2, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterMagmaBarrier1"] = { + name = "Magma Barrier", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + "Monster creates a 90% damage absorption barrier that explodes after taking a certain amount of damage, dealing Fire Damage", + }, + modList = { + }, +} + +mods["PlayerMonsterMagmaBarrier1"] = { + name = "Magma Barrier", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterFlamewaller1"] = { + name = "Conjures Flamewalls", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + "Monster creates circular walls of Fire that deal damage to enemies standing in them.", + }, + modList = { + }, +} + +mods["PlayerMonsterFlamewaller1"] = { + name = "Conjures Flamewalls", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterLightningStorms1"] = { + name = "Conjures Lightning Storms", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["PlayerMonsterLightningStorms1"] = { + name = "Conjures Lightning Storms", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterElementalWaller1"] = { + name = "Conjures Elemental Hazards", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["PlayerMonsterElementalWaller1"] = { + name = "Conjures Elemental Hazards", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterVolatilePlants1"] = { + name = "Volatile Plants", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + "Monster periodically creates Volatile Plants, releasing orbs that move towards enemies; exploding when they get close enough; dealing Chaos Damage.", + }, + modList = { + }, +} + +mods["PlayerMonsterVolatilePlants1"] = { + name = "Volatile Plants", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterVolatilePlants2"] = { + name = "Empowering Volatile Plants", + rollable = true, + type = "Prefix", + tier = 2, + statDescriptions = { + "Monster periodically creates Powerful Volatile Plants, releasing orbs that move towards enemies; exploding when they get close enough; dealing Chaos Damage.", + }, + modList = { + }, +} + +mods["PlayerMonsterVolatilePlants2"] = { + name = "Empowering Volatile Plants", + type = "Prefix", + tier = 2, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterVolatileRocks1"] = { + name = "Volatile Crag", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + "Monster periodically creates Volatile Crag that moves towards enemies; exploding when they get close enough; dealing Fire Damage.", + }, + modList = { + }, +} + +mods["PlayerMonsterVolatileRocks1"] = { + name = "Volatile Crag", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterVolatileRocks2"] = { + name = "Empowering Volatile Crag", + rollable = true, + type = "Prefix", + tier = 2, + statDescriptions = { + "Monster periodically creates Powerful Volatile Crag that moves towards enemies; exploding when they get close enough; dealing Fire Damage.", + }, + modList = { + }, +} + +mods["PlayerMonsterVolatileRocks2"] = { + name = "Empowering Volatile Crag", + type = "Prefix", + tier = 2, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterProximalTangibility1"] = { + name = "Proximal Tangibility", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + "Monster cannot be damaged by enemies any further than 3 metres from them.", + }, + modList = { + -- MonsterProximalTangibility1 [ignore_cannot_be_damaged_by_enemies = 0] + }, +} + +mods["PlayerMonsterProximalTangibility1"] = { + name = "Proximal Tangibility", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + -- PlayerMonsterProximalTangibility1 [ignore_cannot_be_damaged_by_enemies = 0] + }, +} + +mods["MonsterBombardier1"] = { + name = "Bombardier", + type = "Prefix", + tier = 1, + statDescriptions = { + "Monster periodically unleashes barrages of Fire projectiles.", + }, + modList = { + }, +} + +mods["PlayerMonsterBombardier1"] = { + name = "Periodically unleashes Fire", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterSoulEater1"] = { + name = "Soul Eater", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + -- MonsterSoulEater1 [grant_actor_scale_+%_to_aura_owner_on_death = 0] + -- MonsterSoulEater1 [grant_attack_speed_+%_to_aura_owner_on_death = 1] + -- MonsterSoulEater1 [grant_damage_reduction_%_to_aura_owner_on_death = -1] + -- MonsterSoulEater1 [soul_is_consumed_on_death = 1] + }, +} + +mods["PlayerMonsterSoulEater1"] = { + name = "Soul Eater", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + -- PlayerMonsterSoulEater1 [grant_actor_scale_+%_to_aura_owner_on_death = 0] + -- PlayerMonsterSoulEater1 [grant_attack_speed_+%_to_aura_owner_on_death = 1] + -- PlayerMonsterSoulEater1 [grant_damage_reduction_%_to_aura_owner_on_death = -1] + -- PlayerMonsterSoulEater1 [soul_is_consumed_on_death = 1] + }, +} + +mods["MonsterAbyssalCrystalMineWall1"] = { + name = "Crystalline Barrier", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["PlayerMonsterAbyssalCrystalMineWall1"] = { + name = "Crystalline Barrier", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterAbyssVolatileRocks1"] = { + name = "Volatile Souls", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["PlayerMonsterAbyssVolatileRocks1"] = { + name = "Volatile Souls", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterAbyssSiphonAura1"] = { + name = "Soul Siphoner", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["PlayerMonsterAbyssSiphonAura1"] = { + name = "Soul Siphoner", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterAbyssLastGasp1"] = { + name = "Kurgal's Last Gasp", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["PlayerMonsterAbyssLastGasp1"] = { + name = "Kurgal's Last Gasp", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterAbyssLightlessFaction1"] = { + name = "Amanamu's Void", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["PlayerMonsterAbyssLightlessFaction1"] = { + name = "Amanamu's Void", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterAbyssLeechAura"] = { + name = "Lifestealer Aura", + type = "Prefix", + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterAbyssApparitionMirage1"] = { + name = "Unstable Revenants", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterAbyssImmuneAura1"] = { + name = "Undying Will", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + -- MonsterAbyssImmuneAura1 [ignore_cannot_be_damaged_by_enemies = 1] + }, +} + +mods["PlayerMonsterAbyssImmuneAura1"] = { + name = "Undying Will", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + -- PlayerMonsterAbyssImmuneAura1 [ignore_cannot_be_damaged_by_enemies = 0] + }, +} + +mods["MonsterAbyssFactionRunes"] = { + name = "Lithomantic Runes", + type = "Prefix", + statDescriptions = { + }, + modList = { + }, +} + +mods["PlayerMonsterAbyssFactionRunes"] = { + name = "Lithomantic Runes", + type = "Prefix", + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterAbyssMeteor"] = { + name = "Meteoric Demise", + type = "Prefix", + statDescriptions = { + }, + modList = { + }, +} + +mods["PlayerMonsterAbyssMeteor"] = { + name = "Meteoric Demise", + type = "Prefix", + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterAbyssApparitionBeamcaster"] = { + name = "Kulemak's Desecration", + type = "Prefix", + statDescriptions = { + }, + modList = { + }, +} + +mods["PlayerMonsterAbyssApparitionBeamcaster"] = { + name = "Kulemak's Desecration", + type = "Prefix", + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterAbyssPitSplitting"] = { + name = "Ulaman's Legion", + type = "Prefix", + statDescriptions = { + }, + modList = { + -- MonsterAbyssPitSplitting [grant_actor_scale_+%_to_aura_owner_on_death = 0] + -- MonsterAbyssPitSplitting [grant_attack_speed_+%_to_aura_owner_on_death = 0] + -- MonsterAbyssPitSplitting [grant_damage_reduction_%_to_aura_owner_on_death = 0] + -- MonsterAbyssPitSplitting [soul_is_consumed_on_death = 0] + }, +} + +mods["PlayerMonsterAbyssPitSplitting"] = { + name = "Ulaman's Legion", + type = "Prefix", + statDescriptions = { + }, + modList = { + -- PlayerMonsterAbyssPitSplitting [grant_actor_scale_+%_to_aura_owner_on_death = 0] + -- PlayerMonsterAbyssPitSplitting [grant_attack_speed_+%_to_aura_owner_on_death = 0] + -- PlayerMonsterAbyssPitSplitting [grant_damage_reduction_%_to_aura_owner_on_death = 0] + -- PlayerMonsterAbyssPitSplitting [soul_is_consumed_on_death = 0] + }, +} + +mods["MonsterAbyssPustuleGround1"] = { + name = "Bubonic Trail", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["PlayerMonsterAbyssPustuleGround1"] = { + name = "Bubonic Trail", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterAbyssGeyserWalls1"] = { + name = "Soulflame Geysers", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["PlayerMonsterAbyssGeyserWalls1"] = { + name = "Soulflame Geysers", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterAbyssShadeWalker1"] = { + name = "Shade Walker", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["PlayerMonsterAbyssShadeWalker1"] = { + name = "Shade Walker", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterAbyssSoulcano1"] = { + name = "Eruption of Souls", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["PlayerMonsterAbyssSoulcano1"] = { + name = "Eruption of Souls", + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + }, +} + +mods["MonsterProximalTangibilityHidden1"] = { + name = "Ethereal", + rollable = true, + type = "Prefix", + tier = 1, + statDescriptions = { + }, + modList = { + -- MonsterProximalTangibilityHidden1 [ignore_cannot_be_damaged_by_enemies = 0] + }, +} diff --git a/src/Export/Scripts/tamedBeastMods.lua b/src/Export/Scripts/tamedBeastMods.lua new file mode 100644 index 0000000000..9b27ac27fe --- /dev/null +++ b/src/Export/Scripts/tamedBeastMods.lua @@ -0,0 +1,177 @@ +local function makeSkillMod(modName, modType, modVal, flags, keywordFlags, ...) + return { + name = modName, + type = modType, + value = modVal, + flags = flags or 0, + keywordFlags = keywordFlags or 0, + ... + } +end +local function makeFlagMod(modName, ...) + return makeSkillMod(modName, "FLAG", true, 0, 0, ...) +end +local function makeSkillDataMod(dataKey, dataValue, ...) + return makeSkillMod("SkillData", "LIST", { key = dataKey, value = dataValue }, 0, 0, ...) +end +dofile("../Data/Global.lua") +local skillStatMap = LoadModule("../Data/SkillStatMap.lua", makeSkillMod, makeFlagMod, makeSkillDataMod) + +if not loadStatFile then + dofile("statdesc.lua") +end +-- Tamed beasts are itemised captured monsters; their mods render with this description file +loadStatFile("monster_stat_descriptions.csd") + +local function tableToString(tbl, pre) + pre = pre or "" + local tableString = "{ " + local outNames = { } + for name in pairs(tbl) do + table.insert(outNames, name) + end + table.sort(outNames) + for _, name in ipairs(outNames) do + if type(tbl[name]) == "table" then + tableString = tableString .. tableToString(tbl[name], pre .. name .. ".") + else + if _ > 1 then + tableString = tableString .. ", " + end + tableString = tableString .. pre .. name .. " = " .. (type(tbl[name]) == "string" and '"' or '') .. tostring(tbl[name]) .. (type(tbl[name]) == "string" and '"' or '') + end + end + return tableString .. " }" +end + +local function escapeString(text) + return text:gsub('\\', '\\\\'):gsub('"', '\\"'):gsub("%s*[\r\n]+%s*", " ") +end + +-- GGG keyword markup is stripped with the shared escapeGGGString (Modules/Common.lua, +-- loaded by the export tool) — the same function the importer applies to incoming lines, +-- so stored display lines always match imported ones verbatim. + +local MONSTER_DOMAIN = 3 -- see modDomains in enums.lua +local GEN_PREFIX = 1 +local GEN_SUFFIX = 2 + +-- Player-facing display names for monster mods; this is the same keyword popup +-- system the character API uses for its "[ModId|Display]" markup. The popup +-- Description is the full explanation shown in-game (good tooltip text). +local popupNames, popupDescriptions = { }, { } +for popup in dat("KeywordPopups"):Rows() do + if popup.Name and popup.Name ~= "" then + popupNames[popup.Id] = popup.Name + end + if popup.Description and popup.Description ~= "" then + popupDescriptions[popup.Id] = popup.Description + end +end +-- Nameplate lines for rare monster mods without keyword popups (e.g. "Periodically +-- unleashes [Cold|Ice]"); the table name is a PoE1 leftover GGG reuses in PoE2 +local archNames = { } +for archMod in dat("ArchnemesisMods"):Rows() do + if archMod.Id and archMod.Name and archMod.Name ~= "" then + archNames[archMod.Id.Id] = archMod.Name + end +end + +local out = io.open("../Data/TamedBeastMods.lua", "w") +out:write('-- This file is automatically generated, do not edit!\n') +out:write('-- Path of Building\n') +out:write('--\n') +out:write('-- Tamed Beast (Companion) Modifier Data\n') +out:write('-- Monster data (c) Grinding Gear Games\n') +out:write('\n') +out:write('local mods, mod, flag = ...\n') + +local exported, unmappedStats = 0, { } +for modRow in dat("Mods"):Rows() do + if modRow.Domain == MONSTER_DOMAIN and (modRow.GenerationType == GEN_PREFIX or modRow.GenerationType == GEN_SUFFIX) and not modRow.Id:match("Royale") then + -- Render description lines at min roll; min == max keeps the text free of "(min-max)" ranges + -- so the importer can match display lines verbatim + local stats = { } + for i = 1, 6 do + if modRow["Stat"..i] then + stats[modRow["Stat"..i].Id] = { min = modRow["Stat"..i.."Value"][1], max = modRow["Stat"..i.."Value"][1] } + end + end + if modRow.Type then + stats.Type = modRow.Type + end + local descLines = describeStats(stats) + local popupName = popupNames[modRow.Id] + local archName = archNames[modRow.Id] and escapeGGGString(archNames[modRow.Id]) + local popupDescription = popupDescriptions[modRow.Id] and escapeGGGString(popupDescriptions[modRow.Id]) + if popupName or archName or descLines[1] or modRow.Name ~= "" then + local name = popupName or archName or (modRow.Name ~= "" and modRow.Name) or descLines[1] + -- A mod can only be rolled (and thus appear on a captured beast) if some monster + -- tag carries a positive spawn weight; script-applied mods (abyss etc.) and the + -- "PlayerMonster*" twins are all-zero. Unnamed placeholders are not selectable. + local rollable = false + for _, weight in ipairs(modRow.SpawnWeight) do + if weight > 0 then + rollable = true + break + end + end + out:write('\nmods["', modRow.Id, '"] = {\n') + out:write('\tname = "', escapeString(name), '",\n') + if rollable and name ~= "TBD" then + out:write('\trollable = true,\n') + end + out:write('\ttype = "', modRow.GenerationType == GEN_PREFIX and "Prefix" or "Suffix", '",\n') + local tier = tonumber(modRow.Id:match("(%d+)$")) + if tier then + out:write('\ttier = ', tier, ',\n') + end + out:write('\tstatDescriptions = {\n') + if archName and archName ~= name then + out:write('\t\t"', escapeString(archName), '",\n') + end + if popupDescription then + out:write('\t\t"', escapeString(popupDescription), '",\n') + end + for _, line in ipairs(descLines) do + out:write('\t\t"', escapeString(line), '",\n') + end + out:write('\t},\n') + out:write('\tmodList = {\n') + for i = 1, 6 do + if modRow["Stat"..i] then + local statId = modRow["Stat"..i].Id + local modStats = ' [' .. statId .. ' = ' .. modRow["Stat"..i.."Value"][1] .. ']' + if skillStatMap[statId] then + local newMod = skillStatMap[statId][1] + out:write('\t\tmod("', newMod.name, '", "', newMod.type, '", ', newMod.value and type(newMod.value) ~= "boolean" and tableToString(newMod.value) or (skillStatMap[statId].value or modRow["Stat"..i.."Value"][1] * (skillStatMap[statId].mult or 1) / (skillStatMap[statId].div or 1)), ', ', newMod.flags or 0, ', ', newMod.keywordFlags or 0) + for _, extra in ipairs(newMod) do + out:write(', ', tableToString(extra)) + end + out:write('), -- ', modRow.Id, modStats, '\n') + else + out:write('\t\t-- ', modRow.Id, modStats, '\n') + unmappedStats[statId] = (unmappedStats[statId] or 0) + 1 + end + end + end + out:write('\t},\n') + out:write('}\n') + exported = exported + 1 + end + end +end +out:close() + +print("Tamed beast mod data exported: " .. exported .. " mods.") +local unmappedList = { } +for statId, count in pairs(unmappedStats) do + table.insert(unmappedList, { statId = statId, count = count }) +end +table.sort(unmappedList, function(a, b) return a.count > b.count end) +if unmappedList[1] then + print(#unmappedList .. " stats had no SkillStatMap entry (emitted as comments):") + for _, entry in ipairs(unmappedList) do + print(" " .. entry.statId .. " (x" .. entry.count .. ")") + end +end diff --git a/src/Modules/CalcPerform.lua b/src/Modules/CalcPerform.lua index 8dcf95a33d..17fd77c9c4 100644 --- a/src/Modules/CalcPerform.lua +++ b/src/Modules/CalcPerform.lua @@ -1019,6 +1019,20 @@ function calcs.perform(env, skipEHP) for _, mod in ipairs(env.player.mainSkill.extraSkillModList) do env.minion.modDB:AddMod(mod) end + -- Rolled monster mods on rare tamed beast companions (imported from tamedBeastProperties). + local mainActiveEffect = env.player.mainSkill.activeEffect + local mainGrantedEffect = mainActiveEffect and mainActiveEffect.grantedEffect + local mainSrcInstance = mainActiveEffect and mainActiveEffect.srcInstance + if mainSrcInstance and mainSrcInstance.tamedBeastModList and mainGrantedEffect and mainGrantedEffect.minionList and mainGrantedEffect.name:match("^Companion") then + for _, beastMod in ipairs(mainSrcInstance.tamedBeastModList) do + local beastModData = beastMod.enabled and beastMod.modId and env.data.tamedBeastMods[beastMod.modId] + if beastModData then + for _, mod in ipairs(beastModData.modList) do + env.minion.modDB:AddMod(mod) + end + end + end + end if env.talismanModList then -- Adding mods provided by "Necromantic Talisman" env.minion.modDB:AddList(env.talismanModList) diff --git a/src/Modules/Data.lua b/src/Modules/Data.lua index 8fc98820d9..659c015cdc 100644 --- a/src/Modules/Data.lua +++ b/src/Modules/Data.lua @@ -996,6 +996,40 @@ for _, minion in pairs(data.minions) do mod.source = "Minion:"..minion.name end end +-- Load tamed beast (companion) monster mods +data.tamedBeastMods = { } +LoadModule("Data/TamedBeastMods", data.tamedBeastMods, makeSkillMod, makeFlagMod) +-- Normalises a beast mod display line for matching; nameplate lines are fixed labels +-- without roll values, so case folding and trimming suffice +function data.normaliseBeastModLine(line) + return (line:lower():gsub("^%s+", ""):gsub("%s+$", "")) +end +-- Secondary index for import lines that don't carry a "[ModId|...]" token; iterate ids in +-- sorted order so collisions deterministically resolve to the lowest id +data.tamedBeastModsByDisplay = { } +do + local sortedIds = { } + for id in pairs(data.tamedBeastMods) do + table.insert(sortedIds, id) + end + table.sort(sortedIds) + for _, id in ipairs(sortedIds) do + local beastMod = data.tamedBeastMods[id] + for _, mod in ipairs(beastMod.modList) do + mod.source = "Beast Mod:"..beastMod.name + end + for _, line in ipairs(beastMod.statDescriptions) do + local key = data.normaliseBeastModLine(line) + if not data.tamedBeastModsByDisplay[key] then + data.tamedBeastModsByDisplay[key] = id + end + end + local nameKey = data.normaliseBeastModLine(beastMod.name) + if not data.tamedBeastModsByDisplay[nameKey] then + data.tamedBeastModsByDisplay[nameKey] = id + end + end +end data.printMissingMinionSkills = function() local missing = { } for _, minion in pairs(data.minions) do From f2593c10320df3faf74008081548e433d5e849e8 Mon Sep 17 00:00:00 2001 From: Leonel Togniolli Date: Wed, 10 Jun 2026 14:06:50 -0300 Subject: [PATCH 2/3] Export companion mods by Player id with per-beast spawn validity Tamed beasts roll "Monster*" mods in the wild, but the itemised companion carries the standalone "PlayerMonster*" twin, which is what the character API reports. The exporter now enumerates ArchnemesisMods and emits only Player mods whose beast-side twin is a rollable prefix/suffix, resolving display text through the twin and replacing the rollable flag with the twin's spawn weight table. Mods whose spawn weights can never roll on the selected beast's tags (e.g. Haste Aura has fast_movement = 0) are ignored by calcs and flagged in red on the skill panel and the selection tooltip. Spectres.lua picks up "boss" monster tags for boss beasts so boss-gated mods (spawn weight boss = 0) validate correctly; the exporter change producing them ships separately. --- spec/System/TestTamedBeastMods_spec.lua | 113 +- src/Classes/SkillsTab.lua | 31 +- src/Data/Spectres.lua | 58 +- src/Data/TamedBeastMods.lua | 2097 ++++++----------------- src/Export/Scripts/tamedBeastMods.lua | 89 +- src/Modules/CalcPerform.lua | 3 +- src/Modules/Data.lua | 13 + 7 files changed, 712 insertions(+), 1692 deletions(-) diff --git a/spec/System/TestTamedBeastMods_spec.lua b/spec/System/TestTamedBeastMods_spec.lua index 501c5d6faf..5fe1888ea8 100644 --- a/spec/System/TestTamedBeastMods_spec.lua +++ b/spec/System/TestTamedBeastMods_spec.lua @@ -7,7 +7,7 @@ describe("TestTamedBeastMods", function() { name = "Monster Modifiers:\n{0}", values = { - { "[MonsterFlaskRemovalAura1|Siphons Flask Charges]\n[MonsterLifeRegenerationRatePercentage1|Regenerates Life]\nPeriodically unleashes [Cold|Ice]\n[MonsterAdditionalProjectiles1|Additional Projectiles]", 0 }, + { "[PlayerMonsterFlaskRemovalAura1|Siphons Flask Charges]\n[PlayerMonsterLifeRegenerationRatePercentage1|Regenerates Life]\nPeriodically unleashes [Cold|Ice]\n[PlayerMonsterAdditionalProjectiles1|Additional Projectiles]", 0 }, }, displayMode = 3, }, @@ -18,13 +18,13 @@ describe("TestTamedBeastMods", function() local list = build.importTab:ParseTamedBeastProperties(sampleProperties) assert.are.equals(4, #list) - assert.are.equals("MonsterFlaskRemovalAura1", list[1].modId) + assert.are.equals("PlayerMonsterFlaskRemovalAura1", list[1].modId) assert.are.equals("Siphons Flask Charges", list[1].display) - assert.are.equals("MonsterLifeRegenerationRatePercentage1", list[2].modId) + assert.are.equals("PlayerMonsterLifeRegenerationRatePercentage1", list[2].modId) -- No leading [ModId|...] token; resolved by display line lookup assert.are.equals("Periodically unleashes Ice", list[3].display) - assert.is_not_nil(list[3].modId) - assert.are.equals("MonsterAdditionalProjectiles1", list[4].modId) + assert.are.equals("PlayerMonsterChilledGroundOnDeath1", list[3].modId) + assert.are.equals("PlayerMonsterAdditionalProjectiles1", list[4].modId) for _, entry in ipairs(list) do assert.True(entry.enabled) end @@ -46,6 +46,63 @@ describe("TestTamedBeastMods", function() end) end) + describe("Exported data shape", function() + it("contains only Player-prefixed companion mods with spawn weights", function() + local count = 0 + for modId, beastMod in pairs(build.data.tamedBeastMods) do + count = count + 1 + assert.is_not_nil(modId:match("^PlayerMonster"), modId.." is not Player-prefixed") + assert.is_nil(beastMod.rollable, modId.." still carries the removed rollable flag") + assert.are.equals("string", type(beastMod.name)) + assert.True(beastMod.type == "Prefix" or beastMod.type == "Suffix") + assert.are.equals("table", type(beastMod.statDescriptions)) + assert.are.equals("table", type(beastMod.modList)) + assert.are.equals("table", type(beastMod.spawnWeights)) + assert.True(#beastMod.spawnWeights > 0, modId.." has no spawn weights") + for _, entry in ipairs(beastMod.spawnWeights) do + assert.are.equals("string", type(entry.tag)) + assert.are.equals("number", type(entry.weight)) + end + end + -- ~68 as of 0.5; loose band so balance patches don't break the suite + assert.True(count >= 50 and count <= 100, "unexpected mod count: "..count) + -- Script-applied twin (generation type 3) must not pass the prefix/suffix filter + assert.is_nil(build.data.tamedBeastMods["PlayerMonsterGlacialPrison1"]) + end) + + it("marks boss-only beasts via the object template is_boss flag", function() + local silverfist = build.data.minions["Metadata/Monsters/Quadrilla/QuadrillaBossMinion1"] + assert.is_not_nil(silverfist) + assert.True(isValueInArray(silverfist.monsterTags, "boss") ~= nil) + local wolf = build.data.minions["WolfMinion"] + assert.is_not_nil(wolf) + assert.is_nil(isValueInArray(wolf.monsterTags, "boss")) + end) + end) + + describe("data.beastModCanSpawn", function() + it("blocks zero-weight tag matches ahead of the default entry", function() + local hasteAura = build.data.tamedBeastMods["PlayerMonsterIncreasedSpeedAura1"] + assert.is_not_nil(hasteAura) + assert.False(build.data.beastModCanSpawn(hasteAura, { "fast_movement", "beast" })) + assert.True(build.data.beastModCanSpawn(hasteAura, { "beast" })) + end) + + it("requires positive-gate tags when the default weight is zero", function() + local alwaysBleeds = build.data.tamedBeastMods["PlayerMonsterAlwaysBleed1"] + assert.is_not_nil(alwaysBleeds) + assert.True(build.data.beastModCanSpawn(alwaysBleeds, { "physical_affinity" })) + assert.False(build.data.beastModCanSpawn(alwaysBleeds, { "beast" })) + assert.False(build.data.beastModCanSpawn(alwaysBleeds, nil)) + end) + + it("falls through to false when no entry matches and there is no default", function() + local beastMod = { spawnWeights = { { tag = "caster", weight = 1 } } } + assert.False(build.data.beastModCanSpawn(beastMod, { "beast" })) + assert.True(build.data.beastModCanSpawn(beastMod, { "caster" })) + end) + end) + describe("SkillsTab persistence", function() it("saves tamed beast mods as Gem child elements", function() build.skillsTab.skillSets[1].socketGroupList = { { @@ -55,7 +112,7 @@ describe("TestTamedBeastMods", function() level = 20, quality = 0, enabled = true, enableGlobal1 = true, enableGlobal2 = true, count = 1, corrupted = false, corruptLevel = 0, tamedBeastModList = { - { modId = "MonsterDamageGainedAsCold1", enabled = true }, + { modId = "PlayerMonsterDamageGainedAsCold1", enabled = true }, { display = "Periodically unleashes Ice", enabled = false }, }, } }, @@ -84,7 +141,7 @@ describe("TestTamedBeastMods", function() end end assert.are.equals(2, #beastModNodes) - assert.are.equals("MonsterDamageGainedAsCold1", beastModNodes[1].attrib.modId) + assert.are.equals("PlayerMonsterDamageGainedAsCold1", beastModNodes[1].attrib.modId) assert.are.equals("true", beastModNodes[1].attrib.enabled) assert.is_nil(beastModNodes[2].attrib.modId) assert.are.equals("Periodically unleashes Ice", beastModNodes[2].attrib.display) @@ -94,7 +151,7 @@ describe("TestTamedBeastMods", function() it("loads TamedBeastMod elements back onto the gem instance", function() local node = { elem = "Skill", attrib = { enabled = "true" }, { elem = "Gem", attrib = { nameSpec = "Companion: Mighty Silverfist", level = "20", quality = "0", enabled = "true" }, - { elem = "TamedBeastMod", attrib = { modId = "MonsterDamageGainedAsCold1", enabled = "true" } }, + { elem = "TamedBeastMod", attrib = { modId = "PlayerMonsterDamageGainedAsCold1", enabled = "true" } }, { elem = "TamedBeastMod", attrib = { display = "Periodically unleashes Ice", enabled = "false" } }, }, } @@ -105,7 +162,7 @@ describe("TestTamedBeastMods", function() local gemInstance = socketGroupList[#socketGroupList].gemList[1] assert.is_not_nil(gemInstance.tamedBeastModList) assert.are.equals(2, #gemInstance.tamedBeastModList) - assert.are.equals("MonsterDamageGainedAsCold1", gemInstance.tamedBeastModList[1].modId) + assert.are.equals("PlayerMonsterDamageGainedAsCold1", gemInstance.tamedBeastModList[1].modId) assert.True(gemInstance.tamedBeastModList[1].enabled) assert.is_nil(gemInstance.tamedBeastModList[2].modId) assert.are.equals("Periodically unleashes Ice", gemInstance.tamedBeastModList[2].display) @@ -125,7 +182,9 @@ describe("TestTamedBeastMods", function() end) describe("Calculation wiring", function() - local beastId = "Metadata/Monsters/Quadrilla/QuadrillaBossMinion1" -- Mighty Silverfist + -- Mighty Silverfist: monsterTags include both fast_movement and boss, so it can + -- never roll Haste Aura (fast_movement = 0) or Regenerates Life (boss = 0) + local beastId = "Metadata/Monsters/Quadrilla/QuadrillaBossMinion1" local function buildCompanionGroup(tamedBeastModList) table.insert(build.beastList, beastId) @@ -148,7 +207,7 @@ describe("TestTamedBeastMods", function() end it("applies enabled beast mods to the companion minion", function() - buildCompanionGroup({ { modId = "MonsterDamageGainedAsCold1", enabled = true } }) + buildCompanionGroup({ { modId = "PlayerMonsterDamageGainedAsCold1", enabled = true } }) local minion = build.calcsTab.mainEnv.minion assert.is_not_nil(minion) @@ -157,7 +216,7 @@ describe("TestTamedBeastMods", function() it("skips disabled and unresolved beast mods", function() local gemInstance = buildCompanionGroup({ - { modId = "MonsterDamageGainedAsCold1", enabled = false }, + { modId = "PlayerMonsterDamageGainedAsCold1", enabled = false }, { display = "Periodically unleashes Ice", enabled = true }, }) @@ -171,6 +230,18 @@ describe("TestTamedBeastMods", function() assert.are.equals(40, build.calcsTab.mainEnv.minion.modDB:Sum("BASE", nil, "DamageGainAsCold")) end) + it("ignores enabled mods the selected beast can never roll", function() + buildCompanionGroup({ + { modId = "PlayerMonsterLifeRegenerationRatePercentage1", enabled = true }, -- boss = 0 + { modId = "PlayerMonsterDamageGainedAsCold1", enabled = true }, + }) + + local minion = build.calcsTab.mainEnv.minion + assert.is_not_nil(minion) + assert.are.equals(0, minion.modDB:Sum("BASE", nil, "LifeRegenPercent")) + assert.are.equals(40, minion.modDB:Sum("BASE", nil, "DamageGainAsCold")) + end) + it("does not affect companions without beast mods", function() buildCompanionGroup(nil) @@ -183,7 +254,7 @@ describe("TestTamedBeastMods", function() build.skillsTab:PasteSocketGroup("Skeletal Sniper 20/0 1") runCallback("OnFrame") local srcInstance = build.skillsTab.socketGroupList[1].gemList[1] - srcInstance.tamedBeastModList = { { modId = "MonsterDamageGainedAsCold1", enabled = true } } + srcInstance.tamedBeastModList = { { modId = "PlayerMonsterDamageGainedAsCold1", enabled = true } } build.buildFlag = true runCallback("OnFrame") @@ -194,11 +265,11 @@ describe("TestTamedBeastMods", function() it("applies entries beyond the fourth and creates UI rows for them", function() buildCompanionGroup({ - { modId = "MonsterIncreasedSpeedAura1", enabled = false }, - { modId = "MonsterLifeRegenerationRatePercentage1", enabled = false }, - { modId = "MonsterAdditionalProjectiles1", enabled = false }, - { modId = "MonsterFlaskRemovalAura1", enabled = false }, - { modId = "MonsterDamageGainedAsCold1", enabled = true }, + { modId = "PlayerMonsterIncreasedSpeedAura1", enabled = false }, + { modId = "PlayerMonsterLifeRegenerationRatePercentage1", enabled = false }, + { modId = "PlayerMonsterAdditionalProjectiles1", enabled = false }, + { modId = "PlayerMonsterFlaskRemovalAura1", enabled = false }, + { modId = "PlayerMonsterDamageGainedAsCold1", enabled = true }, }) assert.are.equals(40, build.calcsTab.mainEnv.minion.modDB:Sum("BASE", nil, "DamageGainAsCold")) @@ -208,7 +279,7 @@ describe("TestTamedBeastMods", function() skillsTab:UpdateBeastModSlots() assert.is_not_nil(skillsTab.beastModSlots[6]) local slot5 = skillsTab.beastModSlots[5] - assert.are.equals("MonsterDamageGainedAsCold1", slot5.select.list[slot5.select.selIndex].modId) + assert.are.equals("PlayerMonsterDamageGainedAsCold1", slot5.select.list[slot5.select.selIndex].modId) end) end) @@ -223,7 +294,7 @@ describe("TestTamedBeastMods", function() properties = { { name = "Level", values = { { "17", 0 } } } }, tamedBeastProperties = { { name = "Monster Modifiers:\n{0}", - values = { { "[MonsterDamageGainedAsCold1|Extra Cold Damage]\n[MonsterIncreasedSpeedAura1|Haste Aura]", 0 } }, + values = { { "[PlayerMonsterDamageGainedAsCold1|Extra Cold Damage]\n[PlayerMonsterIncreasedSpeedAura1|Haste Aura]", 0 } }, displayMode = 3, } }, } }, @@ -246,7 +317,7 @@ describe("TestTamedBeastMods", function() gem = build.skillsTab.socketGroupList[1].gemList[1] assert.are.equals(2, #gem.tamedBeastModList) - assert.are.equals("MonsterDamageGainedAsCold1", gem.tamedBeastModList[1].modId) + assert.are.equals("PlayerMonsterDamageGainedAsCold1", gem.tamedBeastModList[1].modId) assert.False(gem.tamedBeastModList[1].enabled) assert.True(gem.tamedBeastModList[2].enabled) end) diff --git a/src/Classes/SkillsTab.lua b/src/Classes/SkillsTab.lua index cbb0fb2537..288df43bc5 100644 --- a/src/Classes/SkillsTab.lua +++ b/src/Classes/SkillsTab.lua @@ -1158,12 +1158,8 @@ function SkillsTabClass:GetBeastModDropList() local sorted = { } local nameCount = { } for modId, beastMod in pairs(self.build.data.tamedBeastMods) do - -- Non-rollable mods (script-applied, player-minion variants, placeholders) stay - -- resolvable on import but are not offered for selection - if beastMod.rollable then - t_insert(sorted, { modId = modId, beastMod = beastMod }) - nameCount[beastMod.name] = (nameCount[beastMod.name] or 0) + 1 - end + t_insert(sorted, { modId = modId, beastMod = beastMod }) + nameCount[beastMod.name] = (nameCount[beastMod.name] or 0) + 1 end table.sort(sorted, function(a, b) if a.beastMod.name ~= b.beastMod.name then @@ -1258,6 +1254,10 @@ function SkillsTabClass:CreateBeastModSlot(index) if not gemInstance then return end + local minion = gemInstance.skillMinion and self.build.data.minions[gemInstance.skillMinion] + if minion and not self.build.data.beastModCanSpawn(value.beastMod, minion.monsterTags) then + tooltip:AddLine(16, "^1Cannot naturally spawn on "..minion.name.." and will be ignored") + end local calcFunc, calcBase = self.build.calcsTab:GetMiscCalculator(self.build) if calcFunc then -- Trial-swap the entry for the compare, restoring both the entry and the @@ -1304,19 +1304,22 @@ function SkillsTabClass:CreateBeastModSlot(index) end self.controls["beastModSlot"..index.."Enable"] = slot.enabled - -- Imported modifier the dropdown can't represent: unknown id, or known but not in the - -- selectable pool. Either way the entry still applies and can be toggled or removed. + -- Per-row problem indicator: an imported entry the data doesn't know (stays toggleable + -- and removable, but applies nothing), or a known mod the selected beast's tags can + -- never roll (ignored by calcs, see CalcPerform) slot.unresolved = new("LabelControl", {"LEFT", slot.enabled, "RIGHT"}, {8, 0, 0, 16}, function() - local entry = getEntry() + local entry, gemInstance = getEntry() if not entry then return "" end - if not entry.modId then - return entry.display and ("^1Unrecognised: ^7"..entry.display) or "" + local beastMod = entry.modId and self.build.data.tamedBeastMods[entry.modId] + if not beastMod then + local display = entry.display or entry.modId + return display and ("^1Unrecognised: ^7"..display) or "" end - local beastMod = self.build.data.tamedBeastMods[entry.modId] - if not (beastMod and beastMod.rollable) then - return "^1Not selectable: ^7"..(beastMod and beastMod.name or entry.modId) + local minion = gemInstance.skillMinion and self.build.data.minions[gemInstance.skillMinion] + if minion and not self.build.data.beastModCanSpawn(beastMod, minion.monsterTags) then + return "^1Cannot spawn on ^7"..minion.name end return "" end) diff --git a/src/Data/Spectres.lua b/src/Data/Spectres.lua index d1ad258eab..48f0e1a4f5 100644 --- a/src/Data/Spectres.lua +++ b/src/Data/Spectres.lua @@ -20584,7 +20584,7 @@ minions["Metadata/Monsters/LeagueIncursionNew/Thaumaturge/VaalThaumaturgeSpear"] minions["Metadata/Monsters/LeagueIncursionNew/MiniBosses/SoulCoreQuadrillaBoss/SoulCoreQuadrillaMinion"] = { name = "Quadrilla Sergeant", - monsterTags = { "2HBluntStone_onhit_audio", "beast", "fast_movement", "humanoid", "incursion_unique_quadrilla", "not_dex", "not_int", "red_blood", "very_fast_movement", }, + monsterTags = { "2HBluntStone_onhit_audio", "beast", "fast_movement", "humanoid", "incursion_unique_quadrilla", "not_dex", "not_int", "red_blood", "very_fast_movement", "boss", }, life = 2.5, baseDamageIgnoresAttackSpeed = true, armour = 0.66, @@ -20626,7 +20626,7 @@ minions["Metadata/Monsters/LeagueIncursionNew/MiniBosses/SoulCoreQuadrillaBoss/S minions["Metadata/Monsters/LeagueIncursionNew/MiniBosses/IncursionChainedBeastBoss/ChainedBeastBossMinion_"] = { name = "Unchained Beast", - monsterTags = { "beast", "Claw_onhit_audio", "incursion_unique_chained_beast", "mammal_beast", "medium_movement", "not_dex", "not_int", "red_blood", }, + monsterTags = { "beast", "Claw_onhit_audio", "incursion_unique_chained_beast", "mammal_beast", "medium_movement", "not_dex", "not_int", "red_blood", "boss", }, life = 2.5, baseDamageIgnoresAttackSpeed = true, armour = 0.66, @@ -20740,7 +20740,7 @@ minions["Metadata/Monsters/LeagueExpeditionNew/Expedition2/HumanoidFaction/VaalF minions["Metadata/Monsters/CrowBell/CrowBellBossMinion1"] = { name = "The Crowbell", - monsterTags = { "beast", "fast_movement", "mammal_beast", "MonsterBlunt_onhit_audio", "red_blood", }, + monsterTags = { "beast", "fast_movement", "mammal_beast", "MonsterBlunt_onhit_audio", "red_blood", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -20815,7 +20815,7 @@ minions["Metadata/Monsters/CrowBell/CrowBellBossMinion1"] = { minions["Metadata/Monsters/CrowBell/CrowBellBossMinion2"] = { name = "The Black Crow", - monsterTags = { "beast", "fast_movement", "mammal_beast", "MonsterBlunt_onhit_audio", "red_blood", }, + monsterTags = { "beast", "fast_movement", "mammal_beast", "MonsterBlunt_onhit_audio", "red_blood", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -20890,7 +20890,7 @@ minions["Metadata/Monsters/CrowBell/CrowBellBossMinion2"] = { minions["Metadata/Monsters/MudBurrower/MudBurrowerHeadBossMinion1"] = { name = "The Devourer", - monsterTags = { "beast", "Beast_onhit_audio", "mammal_beast", "medium_movement", "not_dex", "not_int", "red_blood", }, + monsterTags = { "beast", "Beast_onhit_audio", "mammal_beast", "medium_movement", "not_dex", "not_int", "red_blood", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -20953,7 +20953,7 @@ minions["Metadata/Monsters/MudBurrower/MudBurrowerHeadBossMinion1"] = { minions["Metadata/Monsters/MudBurrower/MudBurrowerHeadBossMinion2"] = { name = "Gorian, the Moving Earth", - monsterTags = { "beast", "Beast_onhit_audio", "mammal_beast", "medium_movement", "not_dex", "not_int", "red_blood", }, + monsterTags = { "beast", "Beast_onhit_audio", "mammal_beast", "medium_movement", "not_dex", "not_int", "red_blood", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -21016,7 +21016,7 @@ minions["Metadata/Monsters/MudBurrower/MudBurrowerHeadBossMinion2"] = { minions["Metadata/Monsters/ChimeraWetlandsBoss/ChimeraWetlandsBossMinion1"] = { name = "Xyclucian, the Chimera", - monsterTags = { "beast", "Claw_onhit_audio", "flying", "mammal_beast", "red_blood", "slow_movement", }, + monsterTags = { "beast", "Claw_onhit_audio", "flying", "mammal_beast", "red_blood", "slow_movement", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -21086,7 +21086,7 @@ minions["Metadata/Monsters/ChimeraWetlandsBoss/ChimeraWetlandsBossMinion1"] = { minions["Metadata/Monsters/ChimeraWetlandsBoss/ChimeraWetlandsBossMinion2"] = { name = "Xilozoma, the Maw-Beast", - monsterTags = { "beast", "Claw_onhit_audio", "flying", "mammal_beast", "red_blood", "slow_movement", }, + monsterTags = { "beast", "Claw_onhit_audio", "flying", "mammal_beast", "red_blood", "slow_movement", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -21156,7 +21156,7 @@ minions["Metadata/Monsters/ChimeraWetlandsBoss/ChimeraWetlandsBossMinion2"] = { minions["Metadata/Monsters/Ultimatum/ChimeraUltimatumBossMinion1"] = { name = "Uxmal, the Beastlord", - monsterTags = { "beast", "Claw_onhit_audio", "flying", "mammal_beast", "red_blood", "slow_movement", }, + monsterTags = { "beast", "Claw_onhit_audio", "flying", "mammal_beast", "red_blood", "slow_movement", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -21220,7 +21220,7 @@ minions["Metadata/Monsters/Ultimatum/ChimeraUltimatumBossMinion1"] = { minions["Metadata/Monsters/Ultimatum/ChimeraUltimatumBossMinion2"] = { name = "Gressor-Kul, the Apex", - monsterTags = { "beast", "Claw_onhit_audio", "flying", "mammal_beast", "red_blood", "slow_movement", }, + monsterTags = { "beast", "Claw_onhit_audio", "flying", "mammal_beast", "red_blood", "slow_movement", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -21284,7 +21284,7 @@ minions["Metadata/Monsters/Ultimatum/ChimeraUltimatumBossMinion2"] = { minions["Metadata/Monsters/Bird2/MutantBird2Minion1"] = { name = "Scourge of the Skies", - monsterTags = { "beast", "Beast_onhit_audio", "flying", "red_blood", "very_slow_movement", }, + monsterTags = { "beast", "Beast_onhit_audio", "flying", "red_blood", "very_slow_movement", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -21368,7 +21368,7 @@ minions["Metadata/Monsters/Bird2/MutantBird2Minion1"] = { minions["Metadata/Monsters/Bird2/MutantBird2Minion2"] = { name = "Chetza, the Feathered Plague", - monsterTags = { "beast", "Beast_onhit_audio", "flying", "red_blood", "very_slow_movement", }, + monsterTags = { "beast", "Beast_onhit_audio", "flying", "red_blood", "very_slow_movement", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -21452,7 +21452,7 @@ minions["Metadata/Monsters/Bird2/MutantBird2Minion2"] = { minions["Metadata/Monsters/HyenaMonster/RathbreakerBossMinion1"] = { name = "Rathbreaker", - monsterTags = { "2HSharpMetal_onhit_audio", "beast", "fast_movement", "mammal_beast", "melee", "physical_affinity", "red_blood", }, + monsterTags = { "2HSharpMetal_onhit_audio", "beast", "fast_movement", "mammal_beast", "melee", "physical_affinity", "red_blood", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -21501,7 +21501,7 @@ minions["Metadata/Monsters/HyenaMonster/RathbreakerBossMinion1"] = { minions["Metadata/Monsters/HyenaMonster/RathbreakerBossMinion2"] = { name = "Caedron, the Hyena Lord", - monsterTags = { "2HSharpMetal_onhit_audio", "beast", "fast_movement", "mammal_beast", "melee", "physical_affinity", "red_blood", }, + monsterTags = { "2HSharpMetal_onhit_audio", "beast", "fast_movement", "mammal_beast", "melee", "physical_affinity", "red_blood", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -21550,7 +21550,7 @@ minions["Metadata/Monsters/HyenaMonster/RathbreakerBossMinion2"] = { minions["Metadata/Monsters/Quadrilla/QuadrillaBossMinion1"] = { name = "Mighty Silverfist", - monsterTags = { "beast", "fast_movement", "mammal_beast", "MonsterBlunt_onhit_audio", "not_dex", "not_int", "red_blood", "very_fast_movement", }, + monsterTags = { "beast", "fast_movement", "mammal_beast", "MonsterBlunt_onhit_audio", "not_dex", "not_int", "red_blood", "very_fast_movement", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -21595,7 +21595,7 @@ minions["Metadata/Monsters/Quadrilla/QuadrillaBossMinion1"] = { minions["Metadata/Monsters/Quadrilla/QuadrillaBossMinion2"] = { name = "Zekoa, the Headcrusher", - monsterTags = { "beast", "fast_movement", "mammal_beast", "MonsterBlunt_onhit_audio", "not_dex", "not_int", "red_blood", "very_fast_movement", }, + monsterTags = { "beast", "fast_movement", "mammal_beast", "MonsterBlunt_onhit_audio", "not_dex", "not_int", "red_blood", "very_fast_movement", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -21640,7 +21640,7 @@ minions["Metadata/Monsters/Quadrilla/QuadrillaBossMinion2"] = { minions["Metadata/Monsters/Quadrilla/IcyQuadrillaBossMinion1"] = { name = "The Abominable Yeti", - monsterTags = { "beast", "fast_movement", "mammal_beast", "MonsterBlunt_onhit_audio", "not_dex", "not_int", "red_blood", "very_fast_movement", }, + monsterTags = { "beast", "fast_movement", "mammal_beast", "MonsterBlunt_onhit_audio", "not_dex", "not_int", "red_blood", "very_fast_movement", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -21692,7 +21692,7 @@ minions["Metadata/Monsters/Quadrilla/IcyQuadrillaBossMinion1"] = { minions["Metadata/Monsters/Quadrilla/IcyQuadrillaBossMinion2"] = { name = "The Frostborn Fiend", - monsterTags = { "beast", "fast_movement", "mammal_beast", "MonsterBlunt_onhit_audio", "not_dex", "not_int", "red_blood", "very_fast_movement", }, + monsterTags = { "beast", "fast_movement", "mammal_beast", "MonsterBlunt_onhit_audio", "not_dex", "not_int", "red_blood", "very_fast_movement", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -21744,7 +21744,7 @@ minions["Metadata/Monsters/Quadrilla/IcyQuadrillaBossMinion2"] = { minions["Metadata/Monsters/GreatWhiteOne/GreatWhiteOneMinion1"] = { name = "Great White One", - monsterTags = { "beast", "fast_movement", "MonsterBlunt_onhit_audio", "not_dex", "not_int", "red_blood", }, + monsterTags = { "beast", "fast_movement", "MonsterBlunt_onhit_audio", "not_dex", "not_int", "red_blood", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -21818,7 +21818,7 @@ minions["Metadata/Monsters/GreatWhiteOne/GreatWhiteOneMinion1"] = { minions["Metadata/Monsters/GreatWhiteOne/GreatWhiteOneMinion2"] = { name = "The Sandstrider", - monsterTags = { "beast", "fast_movement", "MonsterBlunt_onhit_audio", "not_dex", "not_int", "red_blood", }, + monsterTags = { "beast", "fast_movement", "MonsterBlunt_onhit_audio", "not_dex", "not_int", "red_blood", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -21892,7 +21892,7 @@ minions["Metadata/Monsters/GreatWhiteOne/GreatWhiteOneMinion2"] = { minions["Metadata/Monsters/Goblins/Beast/ArenaBeastBossMinion1_"] = { name = "The Ravenous Fang", - monsterTags = { "beast", "Claw_onhit_audio", "mammal_beast", "medium_movement", "not_dex", "not_int", "red_blood", }, + monsterTags = { "beast", "Claw_onhit_audio", "mammal_beast", "medium_movement", "not_dex", "not_int", "red_blood", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -21961,7 +21961,7 @@ minions["Metadata/Monsters/Goblins/Beast/ArenaBeastBossMinion1_"] = { minions["Metadata/Monsters/ChaosGodOwlBoss/ChaosGodOwlBossMinion"] = { name = "Bahlak, the Sky Seer", - monsterTags = { "beast", "Beast_onhit_audio", "flying", "not_str", "red_blood", "slow_movement", }, + monsterTags = { "beast", "Beast_onhit_audio", "flying", "not_str", "red_blood", "slow_movement", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -22025,7 +22025,7 @@ minions["Metadata/Monsters/ChaosGodOwlBoss/ChaosGodOwlBossMinion"] = { minions["Metadata/Monsters/ChaosGodOwlBoss/IcyOwlBossMinion1"] = { name = "Rakkar, the Frozen Talon", - monsterTags = { "beast", "Beast_onhit_audio", "flying", "not_str", "red_blood", "slow_movement", }, + monsterTags = { "beast", "Beast_onhit_audio", "flying", "not_str", "red_blood", "slow_movement", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -22088,7 +22088,7 @@ minions["Metadata/Monsters/ChaosGodOwlBoss/IcyOwlBossMinion1"] = { minions["Metadata/Monsters/ChaosGodOwlBoss/IcyOwlBossMinion2"] = { name = "Thraeven, Wing of Winter", - monsterTags = { "beast", "Beast_onhit_audio", "flying", "not_str", "red_blood", "slow_movement", }, + monsterTags = { "beast", "Beast_onhit_audio", "flying", "not_str", "red_blood", "slow_movement", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -22151,7 +22151,7 @@ minions["Metadata/Monsters/ChaosGodOwlBoss/IcyOwlBossMinion2"] = { minions["Metadata/Monsters/MarakethSanctumTrial/Boss/Shakari/ShakariMinion1_"] = { name = "Ashar, the Sand Mother", - monsterTags = { "beast", "fast_movement", "insect", "MonsterStab_onhit_audio", "not_dex", "not_int", "red_blood", "sanctum_monster", "very_fast_movement", }, + monsterTags = { "beast", "fast_movement", "insect", "MonsterStab_onhit_audio", "not_dex", "not_int", "red_blood", "sanctum_monster", "very_fast_movement", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -22203,7 +22203,7 @@ minions["Metadata/Monsters/MarakethSanctumTrial/Boss/Shakari/ShakariMinion1_"] = minions["Metadata/Monsters/MarakethSanctumTrial/Boss/Shakari/ShakariMinion2"] = { name = "Karash, The Dune Dweller", - monsterTags = { "beast", "fast_movement", "insect", "MonsterStab_onhit_audio", "not_dex", "not_int", "red_blood", "sanctum_monster", "very_fast_movement", }, + monsterTags = { "beast", "fast_movement", "insect", "MonsterStab_onhit_audio", "not_dex", "not_int", "red_blood", "sanctum_monster", "very_fast_movement", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -22255,7 +22255,7 @@ minions["Metadata/Monsters/MarakethSanctumTrial/Boss/Shakari/ShakariMinion2"] = minions["Metadata/Monsters/Goblins/Beast/FireBeastBoss/FireBeastBossMinion1"] = { name = "Vornas, the Fell Flame", - monsterTags = { "beast", "Claw_onhit_audio", "fast_movement", "fire", "mammal_beast", "not_dex", "not_int", }, + monsterTags = { "beast", "Claw_onhit_audio", "fast_movement", "fire", "mammal_beast", "not_dex", "not_int", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -22314,7 +22314,7 @@ minions["Metadata/Monsters/Goblins/Beast/FireBeastBoss/FireBeastBossMinion1"] = minions["Metadata/Monsters/Goblins/Beast/FireBeastBoss/FireBeastBossMinion2"] = { name = "Morvak, the Infernal", - monsterTags = { "beast", "Claw_onhit_audio", "fast_movement", "fire", "mammal_beast", "not_dex", "not_int", }, + monsterTags = { "beast", "Claw_onhit_audio", "fast_movement", "fire", "mammal_beast", "not_dex", "not_int", "boss", }, extraFlags = { recommendedBeast = true, }, @@ -22373,7 +22373,7 @@ minions["Metadata/Monsters/Goblins/Beast/FireBeastBoss/FireBeastBossMinion2"] = minions["Metadata/Monsters/MarakethSanctumTrial/Boss/Shakari/ShakariDuoMinion"] = { name = "Akthi, the Final Sting", - monsterTags = { "beast", "fast_movement", "insect", "MonsterStab_onhit_audio", "not_dex", "not_int", "red_blood", "very_fast_movement", }, + monsterTags = { "beast", "fast_movement", "insect", "MonsterStab_onhit_audio", "not_dex", "not_int", "red_blood", "very_fast_movement", "boss", }, extraFlags = { recommendedBeast = true, }, diff --git a/src/Data/TamedBeastMods.lua b/src/Data/TamedBeastMods.lua index 8448f01f6b..09ca89f35c 100644 --- a/src/Data/TamedBeastMods.lua +++ b/src/Data/TamedBeastMods.lua @@ -6,40 +6,19 @@ local mods, mod, flag = ... -mods["MonsterDamageGainedAsFire1"] = { - name = "Extra Fire Damage", - rollable = true, - type = "Suffix", - tier = 1, - statDescriptions = { - "Monster Gains 40% of damage as extra Fire damage.", - }, - modList = { - mod("DamageGainAsFire", "BASE", 40, 0, 0), -- MonsterDamageGainedAsFire1 [non_skill_base_all_damage_%_to_gain_as_fire = 40] - }, -} - mods["PlayerMonsterDamageGainedAsFire1"] = { name = "Extra Fire Damage", type = "Suffix", tier = 1, - statDescriptions = { - }, - modList = { - mod("DamageGainAsFire", "BASE", 40, 0, 0), -- PlayerMonsterDamageGainedAsFire1 [non_skill_base_all_damage_%_to_gain_as_fire = 40] + spawnWeights = { + { tag = "fire_affinity", weight = 1 }, + { tag = "default", weight = 1 }, }, -} - -mods["MonsterDamageGainedAsCold1"] = { - name = "Extra Cold Damage", - rollable = true, - type = "Suffix", - tier = 1, statDescriptions = { - "Monster Gains 40% of damage as extra Cold damage.", + "Monster Gains 40% of damage as extra Fire damage.", }, modList = { - mod("DamageGainAsCold", "BASE", 40, 0, 0), -- MonsterDamageGainedAsCold1 [non_skill_base_all_damage_%_to_gain_as_cold = 40] + mod("DamageGainAsFire", "BASE", 40, 0, 0), -- PlayerMonsterDamageGainedAsFire1 [non_skill_base_all_damage_%_to_gain_as_fire = 40] }, } @@ -47,23 +26,15 @@ mods["PlayerMonsterDamageGainedAsCold1"] = { name = "Extra Cold Damage", type = "Suffix", tier = 1, - statDescriptions = { - }, - modList = { - mod("DamageGainAsCold", "BASE", 40, 0, 0), -- PlayerMonsterDamageGainedAsCold1 [non_skill_base_all_damage_%_to_gain_as_cold = 40] + spawnWeights = { + { tag = "cold_affinity", weight = 1 }, + { tag = "default", weight = 1 }, }, -} - -mods["MonsterDamageGainedAsLightning1"] = { - name = "Extra Lightning Damage", - rollable = true, - type = "Suffix", - tier = 1, statDescriptions = { - "Monster Gains 40% of damage as extra Lightning damage.", + "Monster Gains 40% of damage as extra Cold damage.", }, modList = { - mod("DamageGainAsLightning", "BASE", 40, 0, 0), -- MonsterDamageGainedAsLightning1 [non_skill_base_all_damage_%_to_gain_as_lightning = 40] + mod("DamageGainAsCold", "BASE", 40, 0, 0), -- PlayerMonsterDamageGainedAsCold1 [non_skill_base_all_damage_%_to_gain_as_cold = 40] }, } @@ -71,24 +42,15 @@ mods["PlayerMonsterDamageGainedAsLightning1"] = { name = "Extra Lightning Damage", type = "Suffix", tier = 1, - statDescriptions = { - }, - modList = { - mod("DamageGainAsLightning", "BASE", 40, 0, 0), -- PlayerMonsterDamageGainedAsLightning1 [non_skill_base_all_damage_%_to_gain_as_lightning = 40] + spawnWeights = { + { tag = "lightning_affinity", weight = 1 }, + { tag = "default", weight = 1 }, }, -} - -mods["MonsterIncreasedSpeed1"] = { - name = "Hasted", - rollable = true, - type = "Suffix", - tier = 1, statDescriptions = { - "Monster has 30% increased Attack, Cast and Movement speed.", + "Monster Gains 40% of damage as extra Lightning damage.", }, modList = { - mod("Speed", "INC", 30, 0, 0), -- MonsterIncreasedSpeed1 [attack_and_cast_speed_+% = 30] - mod("MovementSpeed", "INC", 30, 0, 0), -- MonsterIncreasedSpeed1 [base_movement_velocity_+% = 30] + mod("DamageGainAsLightning", "BASE", 40, 0, 0), -- PlayerMonsterDamageGainedAsLightning1 [non_skill_base_all_damage_%_to_gain_as_lightning = 40] }, } @@ -96,7 +58,12 @@ mods["PlayerMonsterIncreasedSpeed1"] = { name = "Hasted", type = "Suffix", tier = 1, + spawnWeights = { + { tag = "fast_movement", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster has 30% increased Attack, Cast and Movement speed.", }, modList = { mod("Speed", "INC", 30, 0, 0), -- PlayerMonsterIncreasedSpeed1 [attack_and_cast_speed_+% = 30] @@ -104,40 +71,18 @@ mods["PlayerMonsterIncreasedSpeed1"] = { }, } -mods["MonsterCriticalStrikeChance1"] = { - name = "Extra Crits", - rollable = true, - type = "Suffix", - tier = 1, - statDescriptions = { - "Monster has 300% increased chance to Critically Hit.", - }, - modList = { - mod("CritChance", "INC", 300, 0, 0), -- MonsterCriticalStrikeChance1 [critical_strike_chance_+% = 300] - }, -} - mods["PlayerMonsterCriticalStrikeChance1"] = { name = "Extra Crits", type = "Suffix", tier = 1, - statDescriptions = { - }, - modList = { - mod("CritChance", "INC", 300, 0, 0), -- PlayerMonsterCriticalStrikeChance1 [critical_strike_chance_+% = 300] + spawnWeights = { + { tag = "default", weight = 1 }, }, -} - -mods["MonsterStunDamageIncrease1"] = { - name = "Stuns", - rollable = true, - type = "Suffix", - tier = 1, statDescriptions = { - "Monster has 100% increased Stun buildup.", + "Monster has 300% increased chance to Critically Hit.", }, modList = { - -- MonsterStunDamageIncrease1 [hit_damage_stun_multiplier_+% = 100] + mod("CritChance", "INC", 300, 0, 0), -- PlayerMonsterCriticalStrikeChance1 [critical_strike_chance_+% = 300] }, } @@ -145,23 +90,15 @@ mods["PlayerMonsterStunDamageIncrease1"] = { name = "Stuns", type = "Suffix", tier = 1, - statDescriptions = { - }, - modList = { - -- PlayerMonsterStunDamageIncrease1 [hit_damage_stun_multiplier_+% = 100] + spawnWeights = { + { tag = "melee", weight = 1 }, + { tag = "default", weight = 1 }, }, -} - -mods["MonsterExtraArmour1"] = { - name = "Armoured", - rollable = true, - type = "Suffix", - tier = 1, statDescriptions = { - "Monster gains extra Armour based off of their Strength.", + "Monster has 100% increased Stun buildup.", }, modList = { - -- MonsterExtraArmour1 [monster_additional_strength_ratio_%_for_armour = 100] + -- PlayerMonsterStunDamageIncrease1 [hit_damage_stun_multiplier_+% = 100] }, } @@ -169,23 +106,15 @@ mods["PlayerMonsterExtraArmour1"] = { name = "Armoured", type = "Suffix", tier = 1, - statDescriptions = { - }, - modList = { - -- PlayerMonsterExtraArmour1 [monster_additional_strength_ratio_%_for_armour = 100] + spawnWeights = { + { tag = "armour", weight = 1 }, + { tag = "default", weight = 1 }, }, -} - -mods["MonsterExtraEvasion1"] = { - name = "Evasive", - rollable = true, - type = "Suffix", - tier = 1, statDescriptions = { - "Monster gains extra Evasion based off of their Dexterity.", + "Monster gains extra Armour based off of their Strength.", }, modList = { - -- MonsterExtraEvasion1 [monster_additional_dexterity_ratio_%_for_evasion = 100] + -- PlayerMonsterExtraArmour1 [monster_additional_strength_ratio_%_for_armour = 100] }, } @@ -193,23 +122,15 @@ mods["PlayerMonsterExtraEvasion1"] = { name = "Evasive", type = "Suffix", tier = 1, - statDescriptions = { - }, - modList = { - -- PlayerMonsterExtraEvasion1 [monster_additional_dexterity_ratio_%_for_evasion = 100] + spawnWeights = { + { tag = "evasion", weight = 1 }, + { tag = "default", weight = 1 }, }, -} - -mods["MonsterExtraEnergyShield1"] = { - name = "Extra Energy Shield", - rollable = true, - type = "Suffix", - tier = 1, statDescriptions = { - "Monster gains 25% of Maximum life as added Energy Shield.", + "Monster gains extra Evasion based off of their Dexterity.", }, modList = { - -- MonsterExtraEnergyShield1 [base_maximum_life_%_to_gain_as_total_energy_shield = 25] + -- PlayerMonsterExtraEvasion1 [monster_additional_dexterity_ratio_%_for_evasion = 100] }, } @@ -217,22 +138,15 @@ mods["PlayerMonsterExtraEnergyShield1"] = { name = "Extra Energy Shield", type = "Suffix", tier = 1, - statDescriptions = { - }, - modList = { - -- PlayerMonsterExtraEnergyShield1 [base_maximum_life_%_to_gain_as_total_energy_shield = 25] + spawnWeights = { + { tag = "energy_shield", weight = 1 }, + { tag = "default", weight = 1 }, }, -} - -mods["MonsterAlwaysPoison1"] = { - name = "Always Poisons", - rollable = true, - type = "Suffix", - tier = 1, statDescriptions = { + "Monster gains 25% of Maximum life as added Energy Shield.", }, modList = { - mod("PoisonChance", "BASE", 100, 0, 0), -- MonsterAlwaysPoison1 [global_poison_on_hit = 1] + -- PlayerMonsterExtraEnergyShield1 [base_maximum_life_%_to_gain_as_total_energy_shield = 25] }, } @@ -240,22 +154,15 @@ mods["PlayerMonsterAlwaysPoison1"] = { name = "Always Poisons", type = "Suffix", tier = 1, - statDescriptions = { - }, - modList = { - mod("PoisonChance", "BASE", 100, 0, 0), -- PlayerMonsterAlwaysPoison1 [global_poison_on_hit = 1] + spawnWeights = { + { tag = "physical_affinity", weight = 1 }, + { tag = "chaos_affinity", weight = 1 }, + { tag = "default", weight = 0 }, }, -} - -mods["MonsterAlwaysBleed1"] = { - name = "Always Bleeds", - rollable = true, - type = "Suffix", - tier = 1, statDescriptions = { }, modList = { - mod("BleedChance", "BASE", 100, 0, 0), -- MonsterAlwaysBleed1 [global_bleed_on_hit = 1] + mod("PoisonChance", "BASE", 100, 0, 0), -- PlayerMonsterAlwaysPoison1 [global_poison_on_hit = 1] }, } @@ -263,39 +170,27 @@ mods["PlayerMonsterAlwaysBleed1"] = { name = "Always Bleeds", type = "Suffix", tier = 1, - statDescriptions = { + spawnWeights = { + { tag = "physical_affinity", weight = 1 }, + { tag = "default", weight = 0 }, }, - modList = { - mod("BleedChance", "BASE", 100, 0, 0), -- PlayerMonsterAlwaysBleed1 [global_bleed_on_hit = 1] - }, -} - -mods["MonsterBurningGroundOnDeath1"] = { - name = "Periodically unleashes Fire", - rollable = true, - type = "Prefix", - tier = 1, statDescriptions = { }, modList = { + mod("BleedChance", "BASE", 100, 0, 0), -- PlayerMonsterAlwaysBleed1 [global_bleed_on_hit = 1] }, } mods["PlayerMonsterBurningGroundOnDeath1"] = { - name = "Burning Ground on Death", + name = "Periodically unleashes Fire", type = "Prefix", tier = 1, - statDescriptions = { - }, - modList = { + spawnWeights = { + { tag = "sanctum_monster", weight = 0 }, + { tag = "titan_boss", weight = 0 }, + { tag = "fire_affinity", weight = 1 }, + { tag = "default", weight = 1 }, }, -} - -mods["MonsterChilledGroundOnDeath1"] = { - name = "Periodically unleashes Ice", - rollable = true, - type = "Prefix", - tier = 1, statDescriptions = { }, modList = { @@ -306,17 +201,11 @@ mods["PlayerMonsterChilledGroundOnDeath1"] = { name = "Periodically unleashes Ice", type = "Prefix", tier = 1, - statDescriptions = { - }, - modList = { + spawnWeights = { + { tag = "titan_boss", weight = 0 }, + { tag = "cold_affinity", weight = 1 }, + { tag = "default", weight = 1 }, }, -} - -mods["MonsterShockedGroundOnDeath1"] = { - name = "Periodically unleashes Lightning", - rollable = true, - type = "Prefix", - tier = 1, statDescriptions = { }, modList = { @@ -327,45 +216,14 @@ mods["PlayerMonsterShockedGroundOnDeath1"] = { name = "Periodically unleashes Lightning", type = "Prefix", tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterImmuneToStun1"] = { - name = "Increased Stun Threshold", - type = "Suffix", - tier = 1, - statDescriptions = { - "Monster cannot be Stunned.", - }, - modList = { - mod("StunImmune", "FLAG", 1, 0, 0), -- MonsterImmuneToStun1 [base_cannot_be_stunned = 1] - }, -} - -mods["PlayerMonsterImmuneToStun1"] = { - name = "Increased Stun Threshold", - type = "Suffix", - tier = 1, - statDescriptions = { - }, - modList = { - mod("StunImmune", "FLAG", 1, 0, 0), -- PlayerMonsterImmuneToStun1 [base_cannot_be_stunned = 1] + spawnWeights = { + { tag = "titan_boss", weight = 0 }, + { tag = "lightning_affinity", weight = 1 }, + { tag = "default", weight = 1 }, }, -} - -mods["MonsterStunResilience1"] = { - name = "Stun Resistant", - rollable = true, - type = "Suffix", - tier = 1, statDescriptions = { - "Monster has 250% increased Stun Threshold.", }, modList = { - mod("StunThreshold", "INC", 250, 0, 0), -- MonsterStunResilience1 [stun_threshold_+% = 250] }, } @@ -373,24 +231,14 @@ mods["PlayerMonsterStunResilience1"] = { name = "Stun Resistant", type = "Suffix", tier = 1, - statDescriptions = { + spawnWeights = { + { tag = "default", weight = 1 }, }, - modList = { - mod("StunThreshold", "INC", 250, 0, 0), -- PlayerMonsterStunResilience1 [stun_threshold_+% = 250] - }, -} - -mods["MonsterFireResistance1"] = { - name = "Fire Resistant", - rollable = true, - type = "Suffix", - tier = 1, statDescriptions = { - "Monster has +50% to Fire Resistance and +10% to Maximum Fire Resistance.", + "Monster has 250% increased Stun Threshold.", }, modList = { - mod("FireResist", "BASE", 50, 0, 0), -- MonsterFireResistance1 [base_fire_damage_resistance_% = 50] - mod("FireResistMax", "BASE", 10, 0, 0), -- MonsterFireResistance1 [base_maximum_fire_damage_resistance_% = 10] + mod("StunThreshold", "INC", 250, 0, 0), -- PlayerMonsterStunResilience1 [stun_threshold_+% = 250] }, } @@ -398,7 +246,12 @@ mods["PlayerMonsterFireResistance1"] = { name = "Fire Resistant", type = "Suffix", tier = 1, + spawnWeights = { + { tag = "fire_affinity", weight = 1 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster has +50% to Fire Resistance and +10% to Maximum Fire Resistance.", }, modList = { mod("FireResist", "BASE", 50, 0, 0), -- PlayerMonsterFireResistance1 [base_fire_damage_resistance_% = 50] @@ -406,25 +259,16 @@ mods["PlayerMonsterFireResistance1"] = { }, } -mods["MonsterColdResistance1"] = { - name = "Cold Resistant", - rollable = true, - type = "Suffix", - tier = 1, - statDescriptions = { - "Monster has +50% to Cold Resistance and +10% to Maximum Cold Resistance.", - }, - modList = { - mod("ColdResist", "BASE", 50, 0, 0), -- MonsterColdResistance1 [base_cold_damage_resistance_% = 50] - mod("ColdResistMax", "BASE", 10, 0, 0), -- MonsterColdResistance1 [base_maximum_cold_damage_resistance_% = 10] - }, -} - mods["PlayerMonsterColdResistance1"] = { name = "Cold Resistant", type = "Suffix", tier = 1, + spawnWeights = { + { tag = "cold_affinity", weight = 1 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster has +50% to Cold Resistance and +10% to Maximum Cold Resistance.", }, modList = { mod("ColdResist", "BASE", 50, 0, 0), -- PlayerMonsterColdResistance1 [base_cold_damage_resistance_% = 50] @@ -432,25 +276,16 @@ mods["PlayerMonsterColdResistance1"] = { }, } -mods["MonsterLightningResistance1"] = { - name = "Lightning Resistant", - rollable = true, - type = "Suffix", - tier = 1, - statDescriptions = { - "Monster has +50% to Lightning Resistance and +10% to Maximum Lightning Resistance.", - }, - modList = { - mod("LightningResist", "BASE", 50, 0, 0), -- MonsterLightningResistance1 [base_lightning_damage_resistance_% = 50] - mod("LightningResistMax", "BASE", 10, 0, 0), -- MonsterLightningResistance1 [base_maximum_lightning_damage_resistance_% = 10] - }, -} - mods["PlayerMonsterLightningResistance1"] = { name = "Lightning Resistant", type = "Suffix", tier = 1, + spawnWeights = { + { tag = "lightning_affinity", weight = 1 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster has +50% to Lightning Resistance and +10% to Maximum Lightning Resistance.", }, modList = { mod("LightningResist", "BASE", 50, 0, 0), -- PlayerMonsterLightningResistance1 [base_lightning_damage_resistance_% = 50] @@ -458,137 +293,81 @@ mods["PlayerMonsterLightningResistance1"] = { }, } -mods["MonsterArmourPenetration1"] = { - name = "Breaks Armour", - rollable = true, - type = "Suffix", - tier = 1, - statDescriptions = { - "Monster Breaks Armour equal to 1000% of Physical Damage dealt.", - }, - modList = { - mod("Condition:CanArmourBreak", "FLAG", 1000, 0, 0, { effectName = "ArmourBreak", effectType = "Buff", type = "GlobalEffect" }), -- MonsterArmourPenetration1 [armour_break_physical_damage_%_dealt_as_armour_break = 1000] - }, -} - mods["PlayerMonsterArmourPenetration1"] = { name = "Breaks Armour", type = "Suffix", tier = 1, + spawnWeights = { + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster Breaks Armour equal to 1000% of Physical Damage dealt.", }, modList = { mod("Condition:CanArmourBreak", "FLAG", 1000, 0, 0, { effectName = "ArmourBreak", effectType = "Buff", type = "GlobalEffect" }), -- PlayerMonsterArmourPenetration1 [armour_break_physical_damage_%_dealt_as_armour_break = 1000] }, } -mods["MonsterIncreasedAccuracy1"] = { +mods["PlayerMonsterIncreasedAccuracy1"] = { name = "Accurate", - rollable = true, type = "Suffix", tier = 1, + spawnWeights = { + { tag = "default", weight = 1 }, + }, statDescriptions = { "Monster has 200% increased Accuracy Rating.", }, modList = { - mod("Accuracy", "INC", 200, 0, 0), -- MonsterIncreasedAccuracy1 [accuracy_rating_+% = 200] + mod("Accuracy", "INC", 200, 0, 0), -- PlayerMonsterIncreasedAccuracy1 [accuracy_rating_+% = 200] }, } -mods["PlayerMonsterIncreasedAccuracy1"] = { - name = "Accurate", +mods["PlayerMonsterDamageGainedAsChaos1"] = { + name = "Extra Chaos Damage", type = "Suffix", tier = 1, + spawnWeights = { + { tag = "chaos_affinity", weight = 1 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster Gains 40% of damage as extra Chaos damage.", }, modList = { - mod("Accuracy", "INC", 200, 0, 0), -- PlayerMonsterIncreasedAccuracy1 [accuracy_rating_+% = 200] + mod("DamageGainAsChaos", "BASE", 40, 0, 0), -- PlayerMonsterDamageGainedAsChaos1 [non_skill_base_all_damage_%_to_gain_as_chaos = 40] }, } -mods["MonsterDamageGainedAsChaos1"] = { - name = "Extra Chaos Damage", - rollable = true, +mods["PlayerMonsterLifeRegenerationRatePercentage1"] = { + name = "Regenerates Life", type = "Suffix", tier = 1, + spawnWeights = { + { tag = "boss", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { - "Monster Gains 40% of damage as extra Chaos damage.", + "Monster Regenerates 2% of Maximum Life per second.", }, modList = { - mod("DamageGainAsChaos", "BASE", 40, 0, 0), -- MonsterDamageGainedAsChaos1 [non_skill_base_all_damage_%_to_gain_as_chaos = 40] + mod("LifeRegenPercent", "BASE", 2, 0, 0), -- PlayerMonsterLifeRegenerationRatePercentage1 [life_regeneration_rate_per_minute_% = 120] }, } -mods["PlayerMonsterDamageGainedAsChaos1"] = { - name = "Extra Chaos Damage", +mods["PlayerMonsterAdditionalProjectiles1"] = { + name = "Additional Projectiles", type = "Suffix", tier = 1, + spawnWeights = { + { tag = "allows_additional_projectiles", weight = 1 }, + { tag = "default", weight = 0 }, + }, statDescriptions = { + "Monster fires 4 additional Projectiles.", }, modList = { - mod("DamageGainAsChaos", "BASE", 40, 0, 0), -- PlayerMonsterDamageGainedAsChaos1 [non_skill_base_all_damage_%_to_gain_as_chaos = 40] - }, -} - -mods["MonsterLifeRegenerationRatePercentage1"] = { - name = "Regenerates Life", - rollable = true, - type = "Suffix", - tier = 1, - statDescriptions = { - "Monster Regenerates 2% of Maximum Life per second.", - }, - modList = { - mod("LifeRegenPercent", "BASE", 2, 0, 0), -- MonsterLifeRegenerationRatePercentage1 [life_regeneration_rate_per_minute_% = 120] - }, -} - -mods["PlayerMonsterLifeRegenerationRatePercentage1"] = { - name = "Regenerates Life", - type = "Suffix", - tier = 1, - statDescriptions = { - }, - modList = { - mod("LifeRegenPercent", "BASE", 2, 0, 0), -- PlayerMonsterLifeRegenerationRatePercentage1 [life_regeneration_rate_per_minute_% = 120] - }, -} - -mods["MonsterAdditionalProjectiles1"] = { - name = "Additional Projectiles", - rollable = true, - type = "Suffix", - tier = 1, - statDescriptions = { - "Monster fires 4 additional Projectiles.", - }, - modList = { - mod("ProjectileCount", "BASE", 4, 0, 0), -- MonsterAdditionalProjectiles1 [number_of_additional_projectiles = 4] - }, -} - -mods["PlayerMonsterAdditionalProjectiles1"] = { - name = "Additional Projectiles", - type = "Suffix", - tier = 1, - statDescriptions = { - }, - modList = { - mod("ProjectileCount", "BASE", 4, 0, 0), -- PlayerMonsterAdditionalProjectiles1 [number_of_additional_projectiles = 4] - }, -} - -mods["MonsterAreaOfEffect1"] = { - name = "Increased Area of Effect", - rollable = true, - type = "Suffix", - tier = 1, - statDescriptions = { - "Monster has 100% Increased Area of Effect.", - "100% more Area of Effect", - }, - modList = { - -- MonsterAreaOfEffect1 [rare_monster_mod_area_of_effect_+%_final = 100] + mod("ProjectileCount", "BASE", 4, 0, 0), -- PlayerMonsterAdditionalProjectiles1 [number_of_additional_projectiles = 4] }, } @@ -596,1585 +375,723 @@ mods["PlayerMonsterAreaOfEffect1"] = { name = "Increased Area of Effect", type = "Suffix", tier = 1, - statDescriptions = { - "100% more Area of Effect", - }, - modList = { - -- PlayerMonsterAreaOfEffect1 [rare_monster_mod_area_of_effect_+%_final = 100] - }, -} - -mods["MonsterIgniteChanceIncrease1"] = { - name = "All Damage Ignites", - rollable = true, - type = "Suffix", - tier = 1, - statDescriptions = { - }, - modList = { - mod("PhysicalCanIgnite", "FLAG", 1, 0, 0), -- MonsterIgniteChanceIncrease1 [all_damage_can_ignite = 1] - mod("EnemyIgniteChance", "BASE", 100, 0, 0), -- MonsterIgniteChanceIncrease1 [always_ignite = 1] - }, -} - -mods["PlayerMonsterIgniteChanceIncrease1"] = { - name = "All Damage Ignites", - type = "Suffix", - tier = 1, - statDescriptions = { - }, - modList = { - mod("PhysicalCanIgnite", "FLAG", 1, 0, 0), -- PlayerMonsterIgniteChanceIncrease1 [all_damage_can_ignite = 1] - mod("EnemyIgniteChance", "BASE", 100, 0, 0), -- PlayerMonsterIgniteChanceIncrease1 [always_ignite = 1] - }, -} - -mods["MonsterFreezeDamageIncrease1"] = { - name = "All Damage Chills", - rollable = true, - type = "Suffix", - tier = 1, - statDescriptions = { - }, - modList = { - -- MonsterFreezeDamageIncrease1 [all_damage_can_chill = 1] - -- MonsterFreezeDamageIncrease1 [chill_minimum_slow_% = 10] - }, -} - -mods["PlayerMonsterFreezeDamageIncrease1"] = { - name = "All Damage Chills", - type = "Suffix", - tier = 1, - statDescriptions = { - }, - modList = { - -- PlayerMonsterFreezeDamageIncrease1 [all_damage_can_chill = 1] - -- PlayerMonsterFreezeDamageIncrease1 [chill_minimum_slow_% = 10] - }, -} - -mods["MonsterShockChanceIncrease1"] = { - name = "All Damage Shocks", - rollable = true, - type = "Suffix", - tier = 1, - statDescriptions = { - }, - modList = { - mod("PhysicalCanShock", "FLAG", 1, 0, 0), -- MonsterShockChanceIncrease1 [all_damage_can_shock = 1] - mod("EnemyShockChance", "BASE", 100, 0, 0), -- MonsterShockChanceIncrease1 [always_shock = 1] - }, -} - -mods["PlayerMonsterShockChanceIncrease1"] = { - name = "All Damage Shocks", - type = "Suffix", - tier = 1, - statDescriptions = { - }, - modList = { - mod("PhysicalCanShock", "FLAG", 1, 0, 0), -- PlayerMonsterShockChanceIncrease1 [all_damage_can_shock = 1] - mod("EnemyShockChance", "BASE", 100, 0, 0), -- PlayerMonsterShockChanceIncrease1 [always_shock = 1] - }, -} - -mods["MonsterBurningGroundTrail1"] = { - name = "Trail of Fire", - rollable = true, - type = "Suffix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["PlayerMonsterBurningGroundTrail1"] = { - name = "Trail of Fire", - type = "Suffix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterChilledGroundTrail1"] = { - name = "Trail of Ice", - rollable = true, - type = "Suffix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["PlayerMonsterChilledGroundTrail1"] = { - name = "Trail of Ice", - type = "Suffix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterShockedGroundTrail1"] = { - name = "Trail of Lightning", - rollable = true, - type = "Suffix", - tier = 1, - statDescriptions = { - "Monster leaves a trail of Shocked Ground as they move.", - }, - modList = { - }, -} - -mods["PlayerMonsterShockedGroundTrail1"] = { - name = "Trail of Lightning", - type = "Suffix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterImmuneToSlow1"] = { - name = "Slow Resistant", - rollable = true, - type = "Suffix", - tier = 1, - statDescriptions = { - "Monster has 50% reduced Slowing Potency of Debuffs on them.", - "50% less Slowing Potency of Debuffs on me", - }, - modList = { - -- MonsterImmuneToSlow1 [monster_slow_potency_+%_final = -50] - }, -} - -mods["PlayerMonsterImmuneToSlow1"] = { - name = "Slow Resistant", - type = "Suffix", - tier = 1, - statDescriptions = { - "50% less Slowing Potency of Debuffs on me", - }, - modList = { - -- PlayerMonsterImmuneToSlow1 [monster_slow_potency_+%_final = -50] - }, -} - -mods["MonsterChaosResistance1"] = { - name = "Chaos Resistant", - rollable = true, - type = "Suffix", - tier = 1, - statDescriptions = { - "Monster has +50% to Chaos Resistance.", - }, - modList = { - mod("ChaosResist", "BASE", 50, 0, 0), -- MonsterChaosResistance1 [base_chaos_damage_resistance_% = 50] - }, -} - -mods["PlayerMonsterChaosResistance1"] = { - name = "Chaos Resistant", - type = "Suffix", - tier = 1, - statDescriptions = { - }, - modList = { - mod("ChaosResist", "BASE", 50, 0, 0), -- PlayerMonsterChaosResistance1 [base_chaos_damage_resistance_% = 50] - }, -} - -mods["MonsterCannotLeech1"] = { - name = "of Congealment", - rollable = true, - type = "Suffix", - tier = 1, - statDescriptions = { - }, - modList = { - -- MonsterCannotLeech1 [life_leeched_from_-permyriad = 6000] - -- MonsterCannotLeech1 [mana_leeched_from_-permyriad = 4000] - -- MonsterCannotLeech1 [energy_shield_leeched_from_-permyriad = 6000] - }, -} - -mods["PlayerMonsterCannotLeech1"] = { - name = "of Congealment", - type = "Suffix", - tier = 1, - statDescriptions = { - }, - modList = { - -- PlayerMonsterCannotLeech1 [life_leeched_from_-permyriad = 6000] - -- PlayerMonsterCannotLeech1 [mana_leeched_from_-permyriad = 4000] - -- PlayerMonsterCannotLeech1 [energy_shield_leeched_from_-permyriad = 6000] - }, -} - -mods["MonsterIsHexproof1"] = { - name = "of Hexproof", - rollable = true, - type = "Suffix", - tier = 1, - statDescriptions = { - "Hexproof", + spawnWeights = { + { tag = "boss", weight = 0 }, + { tag = "allows_inc_aoe", weight = 1 }, + { tag = "default", weight = 0 }, }, - modList = { - -- MonsterIsHexproof1 [hexproof = 1] - }, -} - -mods["PlayerMonsterIsHexproof1"] = { - name = "of Hexproof", - type = "Suffix", - tier = 1, statDescriptions = { - "Hexproof", - }, - modList = { - -- PlayerMonsterIsHexproof1 [hexproof = 1] - }, -} - -mods["MonsterAdditionalChains1"] = { - name = "of Chaining", - rollable = true, - type = "Suffix", - tier = 1, - statDescriptions = { - }, - modList = { - mod("ChainCountMax", "BASE", 2, 0, 0), -- MonsterAdditionalChains1 [number_of_chains = 2] - -- MonsterAdditionalChains1 [projectile_chain_from_terrain_chance_% = 50] - }, -} - -mods["PlayerMonsterAdditionalChains1"] = { - name = "of Chaining", - type = "Suffix", - tier = 1, - statDescriptions = { - }, - modList = { - mod("ChainCountMax", "BASE", 2, 0, 0), -- PlayerMonsterAdditionalChains1 [number_of_chains = 2] - -- PlayerMonsterAdditionalChains1 [projectile_chain_from_terrain_chance_% = 50] - }, -} - -mods["MonsterProjectilesGainDamage1"] = { - name = "of Far Shot", - rollable = true, - type = "Suffix", - tier = 1, - statDescriptions = { - }, - modList = { - -- MonsterProjectilesGainDamage1 [projectile_damage_+%_max_as_distance_travelled_increases = 60] - }, -} - -mods["PlayerMonsterProjectilesGainDamage1"] = { - name = "of Far Shot", - type = "Suffix", - tier = 1, - statDescriptions = { - }, - modList = { - -- PlayerMonsterProjectilesGainDamage1 [projectile_damage_+%_max_as_distance_travelled_increases = 60] - }, -} - -mods["MonsterModReducedCritMulti1"] = { - name = "Crit Resistant", - rollable = true, - type = "Suffix", - tier = 1, - statDescriptions = { - "Hits against this Monster have 80% reduced Critical Damage Bonus.", - }, - modList = { - mod("SelfCritMultiplier", "INC", -80, 0, 0), -- MonsterModReducedCritMulti1 [base_self_critical_strike_multiplier_-% = 80] - }, -} - -mods["PlayerMonsterModReducedCritMulti1"] = { - name = "Crit Resistant", - type = "Suffix", - tier = 1, - statDescriptions = { - }, - modList = { - mod("SelfCritMultiplier", "INC", -80, 0, 0), -- PlayerMonsterModReducedCritMulti1 [base_self_critical_strike_multiplier_-% = 80] - }, -} - -mods["MonsterLastGasp1"] = { - name = "TBD", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - -- MonsterLastGasp1 [retaliation_godmode_ghost_duration_ms = 10000] - -- MonsterLastGasp1 [corpse_cannot_be_destroyed = 1] - -- MonsterLastGasp1 [cannot_be_dominated = 1] - }, -} - -mods["PlayerMonsterLastGasp1"] = { - name = "TBD", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - -- PlayerMonsterLastGasp1 [retaliation_godmode_ghost_duration_ms = 10000] - -- PlayerMonsterLastGasp1 [corpse_cannot_be_destroyed = 1] - -- PlayerMonsterLastGasp1 [cannot_be_dominated = 1] - }, -} - -mods["MonsterFlameBeacons1"] = { - name = "Periodic Fire Explosions", - rollable = true, - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["PlayerMonsterFlameBeacons1"] = { - name = "Periodic Fire Explosions", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterFrostBeacons1"] = { - name = "Periodic Cold Explosions", - rollable = true, - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["PlayerMonsterFrostBeacons1"] = { - name = "Periodic Cold Explosions", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterLightningBeacons1"] = { - name = "Periodic Lightning Explosions", - rollable = true, - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["PlayerMonsterLightningBeacons1"] = { - name = "Periodic Lightning Explosions", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterStrongerMinions1"] = { - name = "Powerful Minions", - rollable = true, - type = "Prefix", - tier = 1, - statDescriptions = { - "Monster's Pack Minions have 25% increased Damage and 50% increased Life.", - }, - modList = { - }, -} - -mods["PlayerMonsterStrongerMinions1"] = { - name = "Powerful Minions", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterPhysicalDamageAura1"] = { - name = "Extra Physical Damage Aura", - rollable = true, - type = "Prefix", - tier = 1, - statDescriptions = { - "Monster creates an Aura that grants 40% increased Physical Damage to Allies within 5 metres.", - }, - modList = { - mod("PhysicalDamage", "INC", 40, 0, 0), -- MonsterPhysicalDamageAura1 [physical_damage_+% = 40] - }, -} - -mods["PlayerMonsterPhysicalDamageAura1"] = { - name = "Extra Physical Damage Aura", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - mod("PhysicalDamage", "INC", 40, 0, 0), -- PlayerMonsterPhysicalDamageAura1 [physical_damage_+% = 40] - }, -} - -mods["MonsterIncreasedSpeedAura1"] = { - name = "Haste Aura", - rollable = true, - type = "Prefix", - tier = 1, - statDescriptions = { - "Monster creates an Aura that grants 20% increased Attack and Cast speed and 10% increased Movement speed to Allies within 5 metres.", - }, - modList = { - mod("Speed", "INC", 25, 0, 0), -- MonsterIncreasedSpeedAura1 [attack_and_cast_speed_+% = 25] - mod("MovementSpeed", "INC", 25, 0, 0), -- MonsterIncreasedSpeedAura1 [base_movement_velocity_+% = 25] - }, -} - -mods["PlayerMonsterIncreasedSpeedAura1"] = { - name = "Haste Aura", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - mod("Speed", "INC", 25, 0, 0), -- PlayerMonsterIncreasedSpeedAura1 [attack_and_cast_speed_+% = 25] - mod("MovementSpeed", "INC", 25, 0, 0), -- PlayerMonsterIncreasedSpeedAura1 [base_movement_velocity_+% = 25] - }, -} - -mods["PlayerMonsterIncreasedSpeedAuraMinion1"] = { - name = "Haste Aura", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - mod("Speed", "INC", 20, 0, 0), -- PlayerMonsterIncreasedSpeedAuraMinion1 [attack_and_cast_speed_+% = 20] - mod("MovementSpeed", "INC", 10, 0, 0), -- PlayerMonsterIncreasedSpeedAuraMinion1 [base_movement_velocity_+% = 10] - }, -} - -mods["MonsterEnergyShieldAura1"] = { - name = "Energy Shield Aura", - rollable = true, - type = "Prefix", - tier = 1, - statDescriptions = { - "Monster creates an Aura that grants 20% of Maximum life as added Energy Shield to Allies within 5 metres.", - }, - modList = { - -- MonsterEnergyShieldAura1 [base_maximum_life_%_to_gain_as_total_energy_shield = 30] - }, -} - -mods["PlayerMonsterEnergyShieldAura1"] = { - name = "Energy Shield Aura", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - -- PlayerMonsterEnergyShieldAura1 [base_maximum_life_%_to_gain_as_total_energy_shield = 30] - }, -} - -mods["PlayerMonsterEnergyShieldAuraMinion1"] = { - name = "Energy Shield Aura", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - -- PlayerMonsterEnergyShieldAuraMinion1 [base_maximum_life_%_to_gain_as_total_energy_shield = 20] - }, -} - -mods["MonsterResistanceAura1"] = { - name = "Elemental Resistance Aura", - rollable = true, - type = "Prefix", - tier = 1, - statDescriptions = { - "Monster creates an Aura that grants +35% to all Elemental Resistances to Allies within 5 metres.", - }, - modList = { - mod("ElementalResist", "BASE", 35, 0, 0), -- MonsterResistanceAura1 [base_resist_all_elements_% = 35] - }, -} - -mods["PlayerMonsterResistanceAura1"] = { - name = "Elemental Resistance Aura", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - mod("ElementalResist", "BASE", 35, 0, 0), -- PlayerMonsterResistanceAura1 [base_resist_all_elements_% = 35] - }, -} - -mods["MonsterTemporalAura1"] = { - name = "Temporal Bubble", - rollable = true, - type = "Prefix", - tier = 1, - statDescriptions = { - "Monster creates an Aura that Debuffs Enemies within 3.2 metres; Slowing by 25%, making effects expire 40% slower and reducing Cooldown Recovery Rate by 60%.", - }, - modList = { - -- MonsterTemporalAura1 [action_speed_-% = 25] - -- MonsterTemporalAura1 [debuff_time_passed_+% = -40] - mod("CooldownRecovery", "INC", -60, 0, 0), -- MonsterTemporalAura1 [base_cooldown_speed_+% = -60] - -- MonsterTemporalAura1 [cannot_be_damaged_by_things_outside_radius = 0] - }, -} - -mods["PlayerMonsterTemporalAura1"] = { - name = "Temporal Bubble", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - -- PlayerMonsterTemporalAura1 [action_speed_-% = 25] - -- PlayerMonsterTemporalAura1 [debuff_time_passed_+% = -40] - mod("CooldownRecovery", "INC", -60, 0, 0), -- PlayerMonsterTemporalAura1 [base_cooldown_speed_+% = -60] - -- PlayerMonsterTemporalAura1 [cannot_be_damaged_by_things_outside_radius = 0] - }, -} - -mods["PlayerMonsterTemporalAuraMinion1"] = { - name = "Temporal Bubble", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - -- PlayerMonsterTemporalAuraMinion1 [action_speed_-% = 12] - -- PlayerMonsterTemporalAuraMinion1 [debuff_time_passed_+% = -40] - mod("CooldownRecovery", "INC", -20, 0, 0), -- PlayerMonsterTemporalAuraMinion1 [base_cooldown_speed_+% = -20] - -- PlayerMonsterTemporalAuraMinion1 [cannot_be_damaged_by_things_outside_radius = 0] - }, -} - -mods["MonsterHinderAura1"] = { - name = "Hinder Aura", - rollable = true, - type = "Prefix", - tier = 1, - statDescriptions = { - "Monster creates an Aura that Hinders enemies within 3.6 metres.", - }, - modList = { - }, -} - -mods["PlayerMonsterHinderAura1"] = { - name = "Hinder Aura", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterPreventRecoveryAura1"] = { - name = "Prevents Recovery Above 50%", - rollable = true, - type = "Prefix", - tier = 1, - statDescriptions = { - "Monster creates an Aura that Debuffs enemies within 4.2 metres, causing their Life and Energy Shield to not be able to recover past 50%.", - }, - modList = { - -- MonsterPreventRecoveryAura1 [cannot_recover_life_or_energy_shield_above_% = 50] - }, -} - -mods["PlayerMonsterPreventRecoveryAura1"] = { - name = "Prevents Recovery Above 50%", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterImmuneAura1"] = { - name = "Periodic Invulnerability Aura", - rollable = true, - type = "Prefix", - tier = 1, - statDescriptions = { - "Monster releases a nova that makes Allies invulnerable for 5 seconds while the monster is alive within 5 metres every 12 seconds.", - }, - modList = { - -- MonsterImmuneAura1 [monster_allies_cannot_take_damage_pulse_owner = 1] - }, -} - -mods["PlayerMonsterImmuneAura1"] = { - name = "Periodic Invulnerability Aura", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - -- PlayerMonsterImmuneAura1 [monster_allies_cannot_take_damage_pulse_owner = 1] - }, -} - -mods["MonsterImmuneAura2"] = { - name = "Empowered Periodic Invulnerability Aura", - rollable = true, - type = "Prefix", - tier = 2, - statDescriptions = { - "Monster releases a nova that makes Allies invulnerable for 5 seconds while the monster is alive within 5 metres every 12 seconds.", - }, - modList = { - -- MonsterImmuneAura2 [monster_allies_cannot_take_damage_pulse_owner = 1] - }, -} - -mods["PlayerMonsterImmuneAura2"] = { - name = "Empowered Periodic Invulnerability Aura", - type = "Prefix", - tier = 2, - statDescriptions = { - }, - modList = { - -- PlayerMonsterImmuneAura2 [monster_allies_cannot_take_damage_pulse_owner = 1] - }, -} - -mods["MonsterImmuneAuraMinion2"] = { - name = "Increased Life", - type = "Prefix", - tier = 2, - statDescriptions = { - "Monster has 50% increased Life.", - }, - modList = { - -- MonsterImmuneAuraMinion2 [base_maximum_life = 0] - -- MonsterImmuneAuraMinion2 [maximum_life_+% = 50] - }, -} - -mods["MonsterManaSiphonAura1"] = { - name = "Siphons Mana and Deals Lightning Damage", - rollable = true, - type = "Prefix", - tier = 1, - statDescriptions = { - "Monster creates a circular effect that drains Mana and deals Lightning Damage over time to enemies near the edge of the circle.", - }, - modList = { - }, -} - -mods["PlayerMonsterManaSiphonAura1"] = { - name = "Siphons Mana and Deals Lightning Damage", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterManaSiphonAura2"] = { - name = "Siphons Mana and Deals Lightning Damage", - rollable = true, - type = "Prefix", - tier = 2, - statDescriptions = { - "Monster creates a circular effect that drains Mana and deals Lightning Damage over time to enemies near the edge of the circle. Additionally, Monster will periodically create separate circles that drain Mana and deal Lightning Damage over time to enemies standing in them.", - }, - modList = { - }, -} - -mods["PlayerMonsterManaSiphonAura2"] = { - name = "Siphons Mana and Deals Lightning Damage", - type = "Prefix", - tier = 2, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterHealingNova1"] = { - name = "Heals Allies and Suppresses Foe Recovery", - rollable = true, - type = "Prefix", - tier = 1, - statDescriptions = { - "Monster releases a nova that reduces Enemy Life and Energy Shield Recovery Rate by 60% and causes Allies to Regenerate 5.5% of Maximum Life per second for 4 seconds within 5 metres every 8 seconds.", - }, - modList = { - }, -} - -mods["PlayerMonsterHealingNova1"] = { - name = "Heals Allies and Suppresses Foe Recovery", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterFlaskRemovalAura1"] = { - name = "Siphons Flask Charges", - rollable = true, - type = "Prefix", - tier = 1, - statDescriptions = { - "Monster creates an Aura that removes 3 Flask and Charm charges from enemies every 3 seconds within 3.6 metres.", - }, - modList = { - -- MonsterFlaskRemovalAura1 [generate_x_charges_for_any_flask_per_minute = -3] - }, -} - -mods["PlayerMonsterFlaskRemovalAura1"] = { - name = "Siphons Flask Charges", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - -- PlayerMonsterFlaskRemovalAura1 [generate_x_charges_for_any_flask_per_minute = -3] - }, -} - -mods["MonsterRevivesMinions1"] = { - name = "Reviving Minions", - rollable = true, - type = "Prefix", - tier = 1, - statDescriptions = { - "Monster periodically revives Pack Minions.", - }, - modList = { - }, -} - -mods["PlayerMonsterRevivesMinions1"] = { - name = "Reviving Minions", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterRevivesMinions2"] = { - name = "Empowered Reviving Minions", - rollable = true, - type = "Prefix", - tier = 2, - statDescriptions = { - "Monster periodically revives Pack Minions with increased Life and Damage.", - }, - modList = { - }, -} - -mods["PlayerMonsterRevivesMinions2"] = { - name = "Empowered Reviving Minions", - type = "Prefix", - tier = 2, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterMinionsTakeLifeInstead1"] = { - name = "Damage Taken From Minions First", - rollable = true, - type = "Prefix", - tier = 1, - statDescriptions = { - "50% of damage taken from Monster is taken from Monster's Pack Minions instead", - }, - modList = { - -- MonsterMinionsTakeLifeInstead1 [damage_removed_from_pack_minions_before_life_or_es_% = 50] - }, -} - -mods["PlayerMonsterMinionsTakeLifeInstead1"] = { - name = "Damage Taken From Minions First", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - -- PlayerMonsterMinionsTakeLifeInstead1 [damage_removed_from_pack_minions_before_life_or_es_% = 50] - }, -} - -mods["MonsterShroudWalker1"] = { - name = "Shroud Walker", - rollable = true, - type = "Prefix", - tier = 1, - statDescriptions = { - "Monster periodically teleports to an enemy they can see, creating a Smoke Cloud where they leave and where they teleport to.", - }, - modList = { - }, -} - -mods["PlayerMonsterShroudWalker1"] = { - name = "Shroud Walker", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterShroudWalker2"] = { - name = "Shroud Walker", - rollable = true, - type = "Prefix", - tier = 2, - statDescriptions = { - "Monster periodically teleports to an enemy they can see, creating a Smoke Cloud where they leave and where they teleport to.", - }, - modList = { - }, -} - -mods["PlayerMonsterShroudWalker2"] = { - name = "Shroud Walker", - type = "Prefix", - tier = 2, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterPeriodicEnrage1"] = { - name = "Periodically Enrages", - rollable = true, - type = "Prefix", - tier = 1, - statDescriptions = { - "Monster periodically Enrages; gaining 30% increased Damage, 25% increased Skill and Movement Speed and 33% less damage taken for 5 seconds every 10 seconds.", - }, - modList = { - }, -} - -mods["PlayerMonsterPeriodicEnrage1"] = { - name = "Periodically Enrages", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterPeriodicEnrage2"] = { - name = "Enraged", - rollable = true, - type = "Prefix", - tier = 2, - statDescriptions = { - "Monster is Enraged; gaining 30% increased Damage, 25% increased Skill and Movement Speed and 33% less damage taken.", - }, - modList = { - }, -} - -mods["PlayerMonsterPeriodicEnrage2"] = { - name = "Enraged", - type = "Prefix", - tier = 2, - statDescriptions = { - }, - modList = { - }, -} - -mods["PlayerMonsterPeriodicEnrageMinion2"] = { - name = "Enraged", - type = "Prefix", - tier = 2, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterCorpseExploder1"] = { - name = "Explodes Nearby Corpses", - rollable = true, - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["PlayerMonsterCorpseExploder1"] = { - name = "Explodes Nearby Corpses", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterLightningMirage1"] = { - name = "Lightning Mirage When Hit", - rollable = true, - type = "Prefix", - tier = 1, - statDescriptions = { - "Monster creates a Mirage when Hit that moves towards enemies and explodes when it gets close enough, dealing Lightning Damage.", - }, - modList = { - }, -} - -mods["PlayerMonsterLightningMirage1"] = { - name = "Lightning Mirage When Hit", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterLightningMirage2"] = { - name = "Lightning Mirages When Hit", - rollable = true, - type = "Prefix", - tier = 2, - statDescriptions = { - "Monster creates Mirages when Hit that move towards enemies and explode when they get close enough, dealing Lightning Damage.", - }, - modList = { - }, -} - -mods["PlayerMonsterLightningMirage2"] = { - name = "Lightning Mirages When Hit", - type = "Prefix", - tier = 2, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterMagmaBarrier1"] = { - name = "Magma Barrier", - rollable = true, - type = "Prefix", - tier = 1, - statDescriptions = { - "Monster creates a 90% damage absorption barrier that explodes after taking a certain amount of damage, dealing Fire Damage", - }, - modList = { - }, -} - -mods["PlayerMonsterMagmaBarrier1"] = { - name = "Magma Barrier", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterFlamewaller1"] = { - name = "Conjures Flamewalls", - rollable = true, - type = "Prefix", - tier = 1, - statDescriptions = { - "Monster creates circular walls of Fire that deal damage to enemies standing in them.", - }, - modList = { - }, -} - -mods["PlayerMonsterFlamewaller1"] = { - name = "Conjures Flamewalls", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterLightningStorms1"] = { - name = "Conjures Lightning Storms", - rollable = true, - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["PlayerMonsterLightningStorms1"] = { - name = "Conjures Lightning Storms", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterElementalWaller1"] = { - name = "Conjures Elemental Hazards", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["PlayerMonsterElementalWaller1"] = { - name = "Conjures Elemental Hazards", - type = "Prefix", - tier = 1, - statDescriptions = { - }, - modList = { - }, -} - -mods["MonsterVolatilePlants1"] = { - name = "Volatile Plants", - rollable = true, - type = "Prefix", - tier = 1, - statDescriptions = { - "Monster periodically creates Volatile Plants, releasing orbs that move towards enemies; exploding when they get close enough; dealing Chaos Damage.", + "Monster has 100% Increased Area of Effect.", + "100% more Area of Effect", }, modList = { + -- PlayerMonsterAreaOfEffect1 [rare_monster_mod_area_of_effect_+%_final = 100] }, } -mods["PlayerMonsterVolatilePlants1"] = { - name = "Volatile Plants", - type = "Prefix", +mods["PlayerMonsterIgniteChanceIncrease1"] = { + name = "All Damage Ignites", + type = "Suffix", tier = 1, + spawnWeights = { + { tag = "fire_affinity", weight = 1 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { }, modList = { + mod("PhysicalCanIgnite", "FLAG", 1, 0, 0), -- PlayerMonsterIgniteChanceIncrease1 [all_damage_can_ignite = 1] + mod("EnemyIgniteChance", "BASE", 100, 0, 0), -- PlayerMonsterIgniteChanceIncrease1 [always_ignite = 1] }, } -mods["MonsterVolatilePlants2"] = { - name = "Empowering Volatile Plants", - rollable = true, - type = "Prefix", - tier = 2, +mods["PlayerMonsterFreezeDamageIncrease1"] = { + name = "All Damage Chills", + type = "Suffix", + tier = 1, + spawnWeights = { + { tag = "cold_affinity", weight = 1 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { - "Monster periodically creates Powerful Volatile Plants, releasing orbs that move towards enemies; exploding when they get close enough; dealing Chaos Damage.", }, modList = { + -- PlayerMonsterFreezeDamageIncrease1 [all_damage_can_chill = 1] + -- PlayerMonsterFreezeDamageIncrease1 [chill_minimum_slow_% = 10] }, } -mods["PlayerMonsterVolatilePlants2"] = { - name = "Empowering Volatile Plants", - type = "Prefix", - tier = 2, +mods["PlayerMonsterShockChanceIncrease1"] = { + name = "All Damage Shocks", + type = "Suffix", + tier = 1, + spawnWeights = { + { tag = "lightning_affinity", weight = 1 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { }, modList = { + mod("PhysicalCanShock", "FLAG", 1, 0, 0), -- PlayerMonsterShockChanceIncrease1 [all_damage_can_shock = 1] + mod("EnemyShockChance", "BASE", 100, 0, 0), -- PlayerMonsterShockChanceIncrease1 [always_shock = 1] }, } -mods["MonsterVolatileRocks1"] = { - name = "Volatile Crag", - rollable = true, - type = "Prefix", +mods["PlayerMonsterBurningGroundTrail1"] = { + name = "Trail of Fire", + type = "Suffix", tier = 1, + spawnWeights = { + { tag = "sanctum_monster", weight = 0 }, + { tag = "immobile", weight = 0 }, + { tag = "ranged", weight = 0 }, + { tag = "fire_affinity", weight = 1 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { - "Monster periodically creates Volatile Crag that moves towards enemies; exploding when they get close enough; dealing Fire Damage.", }, modList = { }, } -mods["PlayerMonsterVolatileRocks1"] = { - name = "Volatile Crag", - type = "Prefix", +mods["PlayerMonsterChilledGroundTrail1"] = { + name = "Trail of Ice", + type = "Suffix", tier = 1, + spawnWeights = { + { tag = "immobile", weight = 0 }, + { tag = "ranged", weight = 0 }, + { tag = "cold_affinity", weight = 1 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { }, modList = { }, } -mods["MonsterVolatileRocks2"] = { - name = "Empowering Volatile Crag", - rollable = true, - type = "Prefix", - tier = 2, +mods["PlayerMonsterShockedGroundTrail1"] = { + name = "Trail of Lightning", + type = "Suffix", + tier = 1, + spawnWeights = { + { tag = "immobile", weight = 0 }, + { tag = "ranged", weight = 0 }, + { tag = "lightning_affinity", weight = 1 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { - "Monster periodically creates Powerful Volatile Crag that moves towards enemies; exploding when they get close enough; dealing Fire Damage.", + "Monster leaves a trail of Shocked Ground as they move.", }, modList = { }, } -mods["PlayerMonsterVolatileRocks2"] = { - name = "Empowering Volatile Crag", - type = "Prefix", - tier = 2, +mods["PlayerMonsterImmuneToSlow1"] = { + name = "Slow Resistant", + type = "Suffix", + tier = 1, + spawnWeights = { + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster has 50% reduced Slowing Potency of Debuffs on them.", + "50% less Slowing Potency of Debuffs on me", }, modList = { + -- PlayerMonsterImmuneToSlow1 [monster_slow_potency_+%_final = -50] }, } -mods["MonsterProximalTangibility1"] = { - name = "Proximal Tangibility", - rollable = true, - type = "Prefix", +mods["PlayerMonsterModReducedCritMulti1"] = { + name = "Crit Resistant", + type = "Suffix", tier = 1, + spawnWeights = { + { tag = "default", weight = 1 }, + }, statDescriptions = { - "Monster cannot be damaged by enemies any further than 3 metres from them.", + "Hits against this Monster have 80% reduced Critical Damage Bonus.", }, modList = { - -- MonsterProximalTangibility1 [ignore_cannot_be_damaged_by_enemies = 0] + mod("SelfCritMultiplier", "INC", -80, 0, 0), -- PlayerMonsterModReducedCritMulti1 [base_self_critical_strike_multiplier_-% = 80] }, } -mods["PlayerMonsterProximalTangibility1"] = { - name = "Proximal Tangibility", - type = "Prefix", +mods["PlayerMonsterChaosResistance1"] = { + name = "Chaos Resistant", + type = "Suffix", tier = 1, + spawnWeights = { + { tag = "chaos_affinity", weight = 1 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster has +50% to Chaos Resistance.", }, modList = { - -- PlayerMonsterProximalTangibility1 [ignore_cannot_be_damaged_by_enemies = 0] + mod("ChaosResist", "BASE", 50, 0, 0), -- PlayerMonsterChaosResistance1 [base_chaos_damage_resistance_% = 50] }, } -mods["MonsterBombardier1"] = { - name = "Bombardier", +mods["PlayerMonsterFlameBeacons1"] = { + name = "Periodic Fire Explosions", type = "Prefix", tier = 1, + spawnWeights = { + { tag = "boss", weight = 0 }, + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { - "Monster periodically unleashes barrages of Fire projectiles.", }, modList = { }, } -mods["PlayerMonsterBombardier1"] = { - name = "Periodically unleashes Fire", +mods["PlayerMonsterFrostBeacons1"] = { + name = "Periodic Cold Explosions", type = "Prefix", tier = 1, + spawnWeights = { + { tag = "boss", weight = 0 }, + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { }, modList = { }, } -mods["MonsterSoulEater1"] = { - name = "Soul Eater", +mods["PlayerMonsterLightningBeacons1"] = { + name = "Periodic Lightning Explosions", type = "Prefix", tier = 1, + spawnWeights = { + { tag = "boss", weight = 0 }, + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { }, modList = { - -- MonsterSoulEater1 [grant_actor_scale_+%_to_aura_owner_on_death = 0] - -- MonsterSoulEater1 [grant_attack_speed_+%_to_aura_owner_on_death = 1] - -- MonsterSoulEater1 [grant_damage_reduction_%_to_aura_owner_on_death = -1] - -- MonsterSoulEater1 [soul_is_consumed_on_death = 1] }, } -mods["PlayerMonsterSoulEater1"] = { - name = "Soul Eater", +mods["PlayerMonsterStrongerMinions1"] = { + name = "Powerful Minions", type = "Prefix", tier = 1, + spawnWeights = { + { tag = "boss", weight = 0 }, + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster's Pack Minions have 25% increased Damage and 50% increased Life.", }, modList = { - -- PlayerMonsterSoulEater1 [grant_actor_scale_+%_to_aura_owner_on_death = 0] - -- PlayerMonsterSoulEater1 [grant_attack_speed_+%_to_aura_owner_on_death = 1] - -- PlayerMonsterSoulEater1 [grant_damage_reduction_%_to_aura_owner_on_death = -1] - -- PlayerMonsterSoulEater1 [soul_is_consumed_on_death = 1] }, } -mods["MonsterAbyssalCrystalMineWall1"] = { - name = "Crystalline Barrier", +mods["PlayerMonsterPhysicalDamageAura1"] = { + name = "Extra Physical Damage Aura", type = "Prefix", tier = 1, + spawnWeights = { + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster creates an Aura that grants 40% increased Physical Damage to Allies within 5 metres.", }, modList = { + mod("PhysicalDamage", "INC", 40, 0, 0), -- PlayerMonsterPhysicalDamageAura1 [physical_damage_+% = 40] }, } -mods["PlayerMonsterAbyssalCrystalMineWall1"] = { - name = "Crystalline Barrier", +mods["PlayerMonsterIncreasedSpeedAura1"] = { + name = "Haste Aura", type = "Prefix", tier = 1, + spawnWeights = { + { tag = "fast_movement", weight = 0 }, + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster creates an Aura that grants 20% increased Attack and Cast speed and 10% increased Movement speed to Allies within 5 metres.", }, modList = { + mod("Speed", "INC", 25, 0, 0), -- PlayerMonsterIncreasedSpeedAura1 [attack_and_cast_speed_+% = 25] + mod("MovementSpeed", "INC", 25, 0, 0), -- PlayerMonsterIncreasedSpeedAura1 [base_movement_velocity_+% = 25] }, } -mods["MonsterAbyssVolatileRocks1"] = { - name = "Volatile Souls", +mods["PlayerMonsterEnergyShieldAura1"] = { + name = "Energy Shield Aura", type = "Prefix", tier = 1, + spawnWeights = { + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster creates an Aura that grants 20% of Maximum life as added Energy Shield to Allies within 5 metres.", }, modList = { + -- PlayerMonsterEnergyShieldAura1 [base_maximum_life_%_to_gain_as_total_energy_shield = 30] }, } -mods["PlayerMonsterAbyssVolatileRocks1"] = { - name = "Volatile Souls", +mods["PlayerMonsterResistanceAura1"] = { + name = "Elemental Resistance Aura", type = "Prefix", tier = 1, + spawnWeights = { + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster creates an Aura that grants +35% to all Elemental Resistances to Allies within 5 metres.", }, modList = { + mod("ElementalResist", "BASE", 35, 0, 0), -- PlayerMonsterResistanceAura1 [base_resist_all_elements_% = 35] }, } -mods["MonsterAbyssSiphonAura1"] = { - name = "Soul Siphoner", +mods["PlayerMonsterTemporalAura1"] = { + name = "Temporal Bubble", type = "Prefix", tier = 1, + spawnWeights = { + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster creates an Aura that Debuffs Enemies within 3.2 metres; Slowing by 25%, making effects expire 40% slower and reducing Cooldown Recovery Rate by 60%.", }, modList = { + -- PlayerMonsterTemporalAura1 [action_speed_-% = 25] + -- PlayerMonsterTemporalAura1 [debuff_time_passed_+% = -40] + mod("CooldownRecovery", "INC", -60, 0, 0), -- PlayerMonsterTemporalAura1 [base_cooldown_speed_+% = -60] + -- PlayerMonsterTemporalAura1 [cannot_be_damaged_by_things_outside_radius = 0] }, } -mods["PlayerMonsterAbyssSiphonAura1"] = { - name = "Soul Siphoner", +mods["PlayerMonsterHinderAura1"] = { + name = "Hinder Aura", type = "Prefix", tier = 1, + spawnWeights = { + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster creates an Aura that Hinders enemies within 3.6 metres.", }, modList = { }, } -mods["MonsterAbyssLastGasp1"] = { - name = "Kurgal's Last Gasp", +mods["PlayerMonsterPreventRecoveryAura1"] = { + name = "Prevents Recovery Above 50%", type = "Prefix", tier = 1, + spawnWeights = { + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster creates an Aura that Debuffs enemies within 4.2 metres, causing their Life and Energy Shield to not be able to recover past 50%.", }, modList = { }, } -mods["PlayerMonsterAbyssLastGasp1"] = { - name = "Kurgal's Last Gasp", +mods["PlayerMonsterImmuneAura1"] = { + name = "Periodic Invulnerability Aura", type = "Prefix", tier = 1, + spawnWeights = { + { tag = "boss", weight = 0 }, + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster releases a nova that makes Allies invulnerable for 5 seconds while the monster is alive within 5 metres every 12 seconds.", }, modList = { + -- PlayerMonsterImmuneAura1 [monster_allies_cannot_take_damage_pulse_owner = 1] }, } -mods["MonsterAbyssLightlessFaction1"] = { - name = "Amanamu's Void", +mods["PlayerMonsterImmuneAura2"] = { + name = "Empowered Periodic Invulnerability Aura", type = "Prefix", - tier = 1, + tier = 2, + spawnWeights = { + { tag = "boss", weight = 0 }, + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster releases a nova that makes Allies invulnerable for 5 seconds while the monster is alive within 5 metres every 12 seconds.", }, modList = { + -- PlayerMonsterImmuneAura2 [monster_allies_cannot_take_damage_pulse_owner = 1] }, } -mods["PlayerMonsterAbyssLightlessFaction1"] = { - name = "Amanamu's Void", +mods["PlayerMonsterManaSiphonAura1"] = { + name = "Siphons Mana and Deals Lightning Damage", type = "Prefix", tier = 1, + spawnWeights = { + { tag = "ranged", weight = 0 }, + { tag = "sanctum_monster", weight = 0 }, + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster creates a circular effect that drains Mana and deals Lightning Damage over time to enemies near the edge of the circle.", }, modList = { }, } -mods["MonsterAbyssLeechAura"] = { - name = "Lifestealer Aura", +mods["PlayerMonsterManaSiphonAura2"] = { + name = "Siphons Mana and Deals Lightning Damage", type = "Prefix", + tier = 2, + spawnWeights = { + { tag = "ranged", weight = 0 }, + { tag = "sanctum_monster", weight = 0 }, + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster creates a circular effect that drains Mana and deals Lightning Damage over time to enemies near the edge of the circle. Additionally, Monster will periodically create separate circles that drain Mana and deal Lightning Damage over time to enemies standing in them.", }, modList = { }, } -mods["MonsterAbyssApparitionMirage1"] = { - name = "Unstable Revenants", +mods["PlayerMonsterHealingNova1"] = { + name = "Heals Allies and Suppresses Foe Recovery", type = "Prefix", tier = 1, + spawnWeights = { + { tag = "boss", weight = 0 }, + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster releases a nova that reduces Enemy Life and Energy Shield Recovery Rate by 60% and causes Allies to Regenerate 5.5% of Maximum Life per second for 4 seconds within 5 metres every 8 seconds.", }, modList = { }, } -mods["MonsterAbyssImmuneAura1"] = { - name = "Undying Will", +mods["PlayerMonsterFlaskRemovalAura1"] = { + name = "Siphons Flask Charges", type = "Prefix", tier = 1, + spawnWeights = { + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster creates an Aura that removes 3 Flask and Charm charges from enemies every 3 seconds within 3.6 metres.", }, modList = { - -- MonsterAbyssImmuneAura1 [ignore_cannot_be_damaged_by_enemies = 1] + -- PlayerMonsterFlaskRemovalAura1 [generate_x_charges_for_any_flask_per_minute = -3] }, } -mods["PlayerMonsterAbyssImmuneAura1"] = { - name = "Undying Will", +mods["PlayerMonsterRevivesMinions1"] = { + name = "Reviving Minions", type = "Prefix", tier = 1, + spawnWeights = { + { tag = "quest_null_monster_mods", weight = 0 }, + { tag = "boss", weight = 0 }, + { tag = "no_minion_revival", weight = 0 }, + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster periodically revives Pack Minions.", }, modList = { - -- PlayerMonsterAbyssImmuneAura1 [ignore_cannot_be_damaged_by_enemies = 0] }, } -mods["MonsterAbyssFactionRunes"] = { - name = "Lithomantic Runes", +mods["PlayerMonsterRevivesMinions2"] = { + name = "Empowered Reviving Minions", type = "Prefix", + tier = 2, + spawnWeights = { + { tag = "quest_null_monster_mods", weight = 0 }, + { tag = "boss", weight = 0 }, + { tag = "no_minion_revival", weight = 0 }, + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster periodically revives Pack Minions with increased Life and Damage.", }, modList = { }, } -mods["PlayerMonsterAbyssFactionRunes"] = { - name = "Lithomantic Runes", +mods["PlayerMonsterMinionsTakeLifeInstead1"] = { + name = "Damage Taken From Minions First", type = "Prefix", + tier = 1, + spawnWeights = { + { tag = "boss", weight = 0 }, + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "50% of damage taken from Monster is taken from Monster's Pack Minions instead", }, modList = { + -- PlayerMonsterMinionsTakeLifeInstead1 [damage_removed_from_pack_minions_before_life_or_es_% = 50] }, } -mods["MonsterAbyssMeteor"] = { - name = "Meteoric Demise", +mods["PlayerMonsterShroudWalker1"] = { + name = "Shroud Walker", type = "Prefix", + tier = 1, + spawnWeights = { + { tag = "sanctum_monster", weight = 0 }, + { tag = "immobile", weight = 0 }, + { tag = "no_shroud_walker", weight = 0 }, + { tag = "boss", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster periodically teleports to an enemy they can see, creating a Smoke Cloud where they leave and where they teleport to.", }, modList = { }, } -mods["PlayerMonsterAbyssMeteor"] = { - name = "Meteoric Demise", +mods["PlayerMonsterShroudWalker2"] = { + name = "Shroud Walker", type = "Prefix", + tier = 2, + spawnWeights = { + { tag = "sanctum_monster", weight = 0 }, + { tag = "immobile", weight = 0 }, + { tag = "no_shroud_walker", weight = 0 }, + { tag = "boss", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster periodically teleports to an enemy they can see, creating a Smoke Cloud where they leave and where they teleport to.", }, modList = { }, } -mods["MonsterAbyssApparitionBeamcaster"] = { - name = "Kulemak's Desecration", +mods["PlayerMonsterPeriodicEnrage1"] = { + name = "Periodically Enrages", type = "Prefix", + tier = 1, + spawnWeights = { + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster periodically Enrages; gaining 30% increased Damage, 25% increased Skill and Movement Speed and 33% less damage taken for 5 seconds every 10 seconds.", }, modList = { }, } -mods["PlayerMonsterAbyssApparitionBeamcaster"] = { - name = "Kulemak's Desecration", +mods["PlayerMonsterPeriodicEnrage2"] = { + name = "Enraged", type = "Prefix", + tier = 2, + spawnWeights = { + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster is Enraged; gaining 30% increased Damage, 25% increased Skill and Movement Speed and 33% less damage taken.", }, modList = { }, } -mods["MonsterAbyssPitSplitting"] = { - name = "Ulaman's Legion", +mods["PlayerMonsterCorpseExploder1"] = { + name = "Explodes Nearby Corpses", type = "Prefix", + tier = 1, + spawnWeights = { + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { }, modList = { - -- MonsterAbyssPitSplitting [grant_actor_scale_+%_to_aura_owner_on_death = 0] - -- MonsterAbyssPitSplitting [grant_attack_speed_+%_to_aura_owner_on_death = 0] - -- MonsterAbyssPitSplitting [grant_damage_reduction_%_to_aura_owner_on_death = 0] - -- MonsterAbyssPitSplitting [soul_is_consumed_on_death = 0] }, } -mods["PlayerMonsterAbyssPitSplitting"] = { - name = "Ulaman's Legion", +mods["PlayerMonsterLightningMirage1"] = { + name = "Lightning Mirage When Hit", type = "Prefix", + tier = 1, + spawnWeights = { + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster creates a Mirage when Hit that moves towards enemies and explodes when it gets close enough, dealing Lightning Damage.", }, modList = { - -- PlayerMonsterAbyssPitSplitting [grant_actor_scale_+%_to_aura_owner_on_death = 0] - -- PlayerMonsterAbyssPitSplitting [grant_attack_speed_+%_to_aura_owner_on_death = 0] - -- PlayerMonsterAbyssPitSplitting [grant_damage_reduction_%_to_aura_owner_on_death = 0] - -- PlayerMonsterAbyssPitSplitting [soul_is_consumed_on_death = 0] }, } -mods["MonsterAbyssPustuleGround1"] = { - name = "Bubonic Trail", +mods["PlayerMonsterLightningMirage2"] = { + name = "Lightning Mirages When Hit", type = "Prefix", - tier = 1, + tier = 2, + spawnWeights = { + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster creates Mirages when Hit that move towards enemies and explode when they get close enough, dealing Lightning Damage.", }, modList = { }, } -mods["PlayerMonsterAbyssPustuleGround1"] = { - name = "Bubonic Trail", +mods["PlayerMonsterMagmaBarrier1"] = { + name = "Magma Barrier", type = "Prefix", tier = 1, + spawnWeights = { + { tag = "sanctum_monster", weight = 0 }, + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster creates a 90% damage absorption barrier that explodes after taking a certain amount of damage, dealing Fire Damage", }, modList = { }, } -mods["MonsterAbyssGeyserWalls1"] = { - name = "Soulflame Geysers", +mods["PlayerMonsterFlamewaller1"] = { + name = "Conjures Flamewalls", type = "Prefix", tier = 1, + spawnWeights = { + { tag = "sanctum_monster", weight = 0 }, + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster creates circular walls of Fire that deal damage to enemies standing in them.", }, modList = { }, } -mods["PlayerMonsterAbyssGeyserWalls1"] = { - name = "Soulflame Geysers", +mods["PlayerMonsterLightningStorms1"] = { + name = "Conjures Lightning Storms", type = "Prefix", tier = 1, + spawnWeights = { + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { }, modList = { }, } -mods["MonsterAbyssShadeWalker1"] = { - name = "Shade Walker", +mods["PlayerMonsterVolatilePlants1"] = { + name = "Volatile Plants", type = "Prefix", tier = 1, + spawnWeights = { + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster periodically creates Volatile Plants, releasing orbs that move towards enemies; exploding when they get close enough; dealing Chaos Damage.", }, modList = { }, } -mods["PlayerMonsterAbyssShadeWalker1"] = { - name = "Shade Walker", +mods["PlayerMonsterVolatilePlants2"] = { + name = "Empowering Volatile Plants", type = "Prefix", - tier = 1, + tier = 2, + spawnWeights = { + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster periodically creates Powerful Volatile Plants, releasing orbs that move towards enemies; exploding when they get close enough; dealing Chaos Damage.", }, modList = { }, } -mods["MonsterAbyssSoulcano1"] = { - name = "Eruption of Souls", +mods["PlayerMonsterVolatileRocks1"] = { + name = "Volatile Crag", type = "Prefix", tier = 1, + spawnWeights = { + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster periodically creates Volatile Crag that moves towards enemies; exploding when they get close enough; dealing Fire Damage.", }, modList = { }, } -mods["PlayerMonsterAbyssSoulcano1"] = { - name = "Eruption of Souls", +mods["PlayerMonsterVolatileRocks2"] = { + name = "Empowering Volatile Crag", type = "Prefix", - tier = 1, + tier = 2, + spawnWeights = { + { tag = "magic", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster periodically creates Powerful Volatile Crag that moves towards enemies; exploding when they get close enough; dealing Fire Damage.", }, modList = { }, } -mods["MonsterProximalTangibilityHidden1"] = { - name = "Ethereal", - rollable = true, +mods["PlayerMonsterProximalTangibility1"] = { + name = "Proximal Tangibility", type = "Prefix", tier = 1, + spawnWeights = { + { tag = "no_proximity_shield", weight = 0 }, + { tag = "boss", weight = 0 }, + { tag = "default", weight = 1 }, + }, statDescriptions = { + "Monster cannot be damaged by enemies any further than 3 metres from them.", }, modList = { - -- MonsterProximalTangibilityHidden1 [ignore_cannot_be_damaged_by_enemies = 0] + -- PlayerMonsterProximalTangibility1 [ignore_cannot_be_damaged_by_enemies = 0] }, } diff --git a/src/Export/Scripts/tamedBeastMods.lua b/src/Export/Scripts/tamedBeastMods.lua index 9b27ac27fe..debf7770fe 100644 --- a/src/Export/Scripts/tamedBeastMods.lua +++ b/src/Export/Scripts/tamedBeastMods.lua @@ -86,46 +86,61 @@ out:write('-- Monster data (c) Grinding Gear Games\n') out:write('\n') out:write('local mods, mod, flag = ...\n') -local exported, unmappedStats = 0, { } -for modRow in dat("Mods"):Rows() do - if modRow.Domain == MONSTER_DOMAIN and (modRow.GenerationType == GEN_PREFIX or modRow.GenerationType == GEN_SUFFIX) and not modRow.Id:match("Royale") then - -- Render description lines at min roll; min == max keeps the text free of "(min-max)" ranges - -- so the importer can match display lines verbatim - local stats = { } - for i = 1, 6 do - if modRow["Stat"..i] then - stats[modRow["Stat"..i].Id] = { min = modRow["Stat"..i.."Value"][1], max = modRow["Stat"..i.."Value"][1] } - end - end - if modRow.Type then - stats.Type = modRow.Type - end - local descLines = describeStats(stats) - local popupName = popupNames[modRow.Id] - local archName = archNames[modRow.Id] and escapeGGGString(archNames[modRow.Id]) - local popupDescription = popupDescriptions[modRow.Id] and escapeGGGString(popupDescriptions[modRow.Id]) - if popupName or archName or descLines[1] or modRow.Name ~= "" then - local name = popupName or archName or (modRow.Name ~= "" and modRow.Name) or descLines[1] - -- A mod can only be rolled (and thus appear on a captured beast) if some monster - -- tag carries a positive spawn weight; script-applied mods (abyss etc.) and the - -- "PlayerMonster*" twins are all-zero. Unnamed placeholders are not selectable. - local rollable = false - for _, weight in ipairs(modRow.SpawnWeight) do +-- Tamed beasts roll their mods in the wild as "Monster*" mods (the ones with real +-- spawn weights); the itemised companion carries the standalone "PlayerMonster*" +-- twin, which is what the character API reports on import, so Player ids are the +-- only ids exported. Display text (keyword popups, nameplate lines) is keyed by +-- the Monster ids, so name and descriptions resolve through the twin; stats come +-- from the Player row itself, which is authoritative for the companion. +local exported, unmappedStats, seen = 0, { }, { } +for archMod in dat("ArchnemesisMods"):Rows() do + local playerRow = archMod.Id + local id = playerRow and playerRow.Id + if id and id:match("^Player") and not seen[id] then + seen[id] = true + local twinId = id:gsub("^Player", "", 1) + local twinRow = dat("Mods"):GetRow("Id", twinId) + -- The mod must exist on the beast side as a rollable prefix/suffix: that is + -- what can appear on a captured beast. + local rollable = false + if twinRow and twinRow.Domain == MONSTER_DOMAIN and (twinRow.GenerationType == GEN_PREFIX or twinRow.GenerationType == GEN_SUFFIX) then + for _, weight in ipairs(twinRow.SpawnWeight) do if weight > 0 then rollable = true break end end - out:write('\nmods["', modRow.Id, '"] = {\n') - out:write('\tname = "', escapeString(name), '",\n') - if rollable and name ~= "TBD" then - out:write('\trollable = true,\n') + end + if rollable then + local stats = { } + for i = 1, 6 do + if playerRow["Stat"..i] then + stats[playerRow["Stat"..i].Id] = { min = playerRow["Stat"..i.."Value"][1], max = playerRow["Stat"..i.."Value"][1] } + end + end + if playerRow.Type then + stats.Type = playerRow.Type end - out:write('\ttype = "', modRow.GenerationType == GEN_PREFIX and "Prefix" or "Suffix", '",\n') - local tier = tonumber(modRow.Id:match("(%d+)$")) + local descLines = describeStats(stats) + local popupName = popupNames[twinId] + local archName = archNames[twinId] and escapeGGGString(archNames[twinId]) + local popupDescription = popupDescriptions[twinId] and escapeGGGString(popupDescriptions[twinId]) + local name = popupName or archName or (twinRow.Name ~= "" and twinRow.Name) or descLines[1] or twinId + out:write('\nmods["', id, '"] = {\n') + out:write('\tname = "', escapeString(name), '",\n') + out:write('\ttype = "', twinRow.GenerationType == GEN_PREFIX and "Prefix" or "Suffix", '",\n') + local tier = tonumber(id:match("(%d+)$")) if tier then out:write('\ttier = ', tier, ',\n') end + if #twinRow.SpawnTags ~= #twinRow.SpawnWeight then + printf("Warning: %s has %d spawn tags but %d weights", twinId, #twinRow.SpawnTags, #twinRow.SpawnWeight) + end + out:write('\tspawnWeights = {\n') + for i, tagRow in ipairs(twinRow.SpawnTags) do + out:write('\t\t{ tag = "', tagRow.Id, '", weight = ', twinRow.SpawnWeight[i] or 0, ' },\n') + end + out:write('\t},\n') out:write('\tstatDescriptions = {\n') if archName and archName ~= name then out:write('\t\t"', escapeString(archName), '",\n') @@ -139,18 +154,18 @@ for modRow in dat("Mods"):Rows() do out:write('\t},\n') out:write('\tmodList = {\n') for i = 1, 6 do - if modRow["Stat"..i] then - local statId = modRow["Stat"..i].Id - local modStats = ' [' .. statId .. ' = ' .. modRow["Stat"..i.."Value"][1] .. ']' + if playerRow["Stat"..i] then + local statId = playerRow["Stat"..i].Id + local modStats = ' [' .. statId .. ' = ' .. playerRow["Stat"..i.."Value"][1] .. ']' if skillStatMap[statId] then local newMod = skillStatMap[statId][1] - out:write('\t\tmod("', newMod.name, '", "', newMod.type, '", ', newMod.value and type(newMod.value) ~= "boolean" and tableToString(newMod.value) or (skillStatMap[statId].value or modRow["Stat"..i.."Value"][1] * (skillStatMap[statId].mult or 1) / (skillStatMap[statId].div or 1)), ', ', newMod.flags or 0, ', ', newMod.keywordFlags or 0) + out:write('\t\tmod("', newMod.name, '", "', newMod.type, '", ', newMod.value and type(newMod.value) ~= "boolean" and tableToString(newMod.value) or (skillStatMap[statId].value or playerRow["Stat"..i.."Value"][1] * (skillStatMap[statId].mult or 1) / (skillStatMap[statId].div or 1)), ', ', newMod.flags or 0, ', ', newMod.keywordFlags or 0) for _, extra in ipairs(newMod) do out:write(', ', tableToString(extra)) end - out:write('), -- ', modRow.Id, modStats, '\n') + out:write('), -- ', id, modStats, '\n') else - out:write('\t\t-- ', modRow.Id, modStats, '\n') + out:write('\t\t-- ', id, modStats, '\n') unmappedStats[statId] = (unmappedStats[statId] or 0) + 1 end end diff --git a/src/Modules/CalcPerform.lua b/src/Modules/CalcPerform.lua index 17fd77c9c4..defdcb0539 100644 --- a/src/Modules/CalcPerform.lua +++ b/src/Modules/CalcPerform.lua @@ -1026,7 +1026,8 @@ function calcs.perform(env, skipEHP) if mainSrcInstance and mainSrcInstance.tamedBeastModList and mainGrantedEffect and mainGrantedEffect.minionList and mainGrantedEffect.name:match("^Companion") then for _, beastMod in ipairs(mainSrcInstance.tamedBeastModList) do local beastModData = beastMod.enabled and beastMod.modId and env.data.tamedBeastMods[beastMod.modId] - if beastModData then + -- A mod the beast's tags can never roll is ignoreed + if beastModData and data.beastModCanSpawn(beastModData, env.minion.minionData.monsterTags) then for _, mod in ipairs(beastModData.modList) do env.minion.modDB:AddMod(mod) end diff --git a/src/Modules/Data.lua b/src/Modules/Data.lua index 659c015cdc..0daf439a2e 100644 --- a/src/Modules/Data.lua +++ b/src/Modules/Data.lua @@ -1004,6 +1004,19 @@ LoadModule("Data/TamedBeastMods", data.tamedBeastMods, makeSkillMod, makeFlagMod function data.normaliseBeastModLine(line) return (line:lower():gsub("^%s+", ""):gsub("%s+$", "")) end + +function data.beastModCanSpawn(beastMod, monsterTags) + local tagSet = { } + for _, tag in ipairs(monsterTags or { }) do + tagSet[tag] = true + end + for _, entry in ipairs(beastMod.spawnWeights) do + if entry.tag == "default" or tagSet[entry.tag] then + return entry.weight > 0 + end + end + return false +end -- Secondary index for import lines that don't carry a "[ModId|...]" token; iterate ids in -- sorted order so collisions deterministically resolve to the lowest id data.tamedBeastModsByDisplay = { } From 13332316eabfc704dfc7b505f2e6e80ef5dcdb45 Mon Sep 17 00:00:00 2001 From: Leonel Togniolli Date: Thu, 11 Jun 2026 22:04:40 -0300 Subject: [PATCH 3/3] Add companion beast and attack controls to the Skills tab The socket group panel for Companion gems gains a beast selector and the list of the beast's attacks. Each attack can be included in Full DPS as its own entry, instead of only the active attack counting: checking an attack pulls the group into Full DPS, unchecking the last one removes it, and enabling Include in Full DPS selects the first attack. The selection is stored per gem by skill id, so it survives beast swaps and falls back to the active attack when nothing matches. Full DPS minion entries now lead with the attack name and show the owning skill as their source line, like the best-ailment entries. Hovering an attack shows its skill data in the player gem tooltip style: tag line, cost, cooldown with stored uses, attack speed, time and damage, crit chance, description, and stat description lines coloured by calc support. Labels gain opt-in tooltip hosting for this; unconfigured labels stay mouse-transparent. Changing the beast renames the Companion gem and refreshes the gem slot display, from both the Skills tab and the sidebar dropdowns. --- spec/System/TestCompanionFullDPS_spec.lua | 324 ++++++++++++++++++++++ src/Classes/GemTooltip.lua | 71 +++++ src/Classes/LabelControl.lua | 22 +- src/Classes/SkillListControl.lua | 1 + src/Classes/SkillsTab.lua | 255 ++++++++++++++++- src/Modules/Build.lua | 5 + src/Modules/CalcActiveSkill.lua | 7 +- src/Modules/Calcs.lua | 135 ++++++--- 8 files changed, 773 insertions(+), 47 deletions(-) create mode 100644 spec/System/TestCompanionFullDPS_spec.lua diff --git a/spec/System/TestCompanionFullDPS_spec.lua b/spec/System/TestCompanionFullDPS_spec.lua new file mode 100644 index 0000000000..244c5829c2 --- /dev/null +++ b/spec/System/TestCompanionFullDPS_spec.lua @@ -0,0 +1,324 @@ +describe("TestCompanionFullDPS", function() + before_each(function() + newBuild() + end) + + -- Mighty Silverfist has exactly two attacks with skill data: + -- [1] MeleeAtAnimationSpeedUnique (Basic Attack), [2] GAQuadrillaBossRectSlam (Pillar Slam) + local beastId = "Metadata/Monsters/Quadrilla/QuadrillaBossMinion1" + local meleeId = "MeleeAtAnimationSpeedUnique" + local slamId = "GAQuadrillaBossRectSlam" + + local function buildCompanionGroup(fullDPSMinionSkills) + table.insert(build.beastList, beastId) + local gemInstance = { + nameSpec = "Companion: Mighty Silverfist", + gemId = "Metadata/Items/Gems/SkillGemSummonBeast", + level = 20, quality = 0, enabled = true, enableGlobal1 = true, enableGlobal2 = true, + count = 1, corrupted = false, corruptLevel = 0, + skillMinion = beastId, + skillMinionCalcs = beastId, + fullDPSMinionSkills = fullDPSMinionSkills, + } + local group = { label = "", enabled = true, includeInFullDPS = true, gemList = { gemInstance } } + table.insert(build.skillsTab.socketGroupList, group) + build.skillsTab:ProcessSocketGroup(group) + build.mainSocketGroup = #build.skillsTab.socketGroupList + build.buildFlag = true + runCallback("OnFrame") + return gemInstance, group + end + + local function companionEntries() + local entries = { } + for _, entry in ipairs(build.calcsTab.mainOutput.SkillDPS or { }) do + if entry.source and entry.source:match("^Companion") then + table.insert(entries, entry) + end + end + return entries + end + + it("counts only the active attack when no selection is made", function() + local gemInstance = buildCompanionGroup(nil) + + local entries = companionEntries() + assert.are.equals(1, #entries) + -- the attack leads the entry; the companion is the source line + assert.are.equals("Basic Attack", entries[1].name) + assert.are.equals("Companion: Mighty Silverfist", entries[1].source) + assert.is_nil(entries[1].skillPart) + assert.True(entries[1].dps > 0) + assert.is_nil(gemInstance.fullDPSMinionSkills) + end) + + it("creates one Full DPS entry per selected attack and sums them", function() + local gemInstance = buildCompanionGroup({ [meleeId] = true, [slamId] = true }) + + local entries = companionEntries() + assert.are.equals(2, #entries) + assert.are_not.equals(entries[1].name, entries[2].name) + local sum = 0 + for _, entry in ipairs(entries) do + assert.are.equals("Companion: Mighty Silverfist", entry.source) + assert.True(entry.dps > 0) + sum = sum + entry.dps * entry.count + end + assert.True(math.abs(build.calcsTab.mainOutput.FullDPS - sum) < 0.01) + -- the calc must not leak its per-pass override or touch persisted fields + assert.are.equals(beastId, gemInstance.skillMinion) + assert.True(gemInstance.fullDPSMinionSkills[meleeId]) + assert.True(gemInstance.fullDPSMinionSkills[slamId]) + for _, activeSkill in ipairs(build.calcsTab.mainEnv.player.activeSkillList) do + assert.is_nil(activeSkill.minionSkillIndexOverride) + end + end) + + it("excludes the active attack when only another attack is selected", function() + buildCompanionGroup({ [slamId] = true }) + + local entries = companionEntries() + assert.are.equals(1, #entries) + assert.are.equals("Pillar Slam", entries[1].name) + end) + + it("falls back to the active attack when no selected id matches the beast", function() + buildCompanionGroup({ NotARealMinionSkillId = true }) + + local entries = companionEntries() + assert.are.equals(1, #entries) + assert.are.equals("Basic Attack", entries[1].name) + end) + + it("lists other minions' entries in the same attack-plus-source format", function() + build.skillsTab:PasteSocketGroup("Skeletal Sniper 20/0 1") + local group = build.skillsTab.socketGroupList[#build.skillsTab.socketGroupList] + group.includeInFullDPS = true + build.buildFlag = true + runCallback("OnFrame") + + local entry + for _, skillEntry in ipairs(build.calcsTab.mainOutput.SkillDPS or { }) do + if skillEntry.source == "Skeletal Sniper Minion" then + entry = skillEntry + end + end + assert.is_not_nil(entry) + assert.is_nil(entry.skillPart) + assert.True(entry.dps > 0) + end) + + it("a single selected attack matches the unselected baseline DPS", function() + buildCompanionGroup(nil) + local baseline = companionEntries()[1] + + newBuild() + buildCompanionGroup({ [meleeId] = true }) + local selected = companionEntries()[1] + + assert.are.equals(baseline.dps, selected.dps) + assert.are.equals(baseline.name, selected.name) + end) + + describe("persistence", function() + it("saves selected attacks as sorted Gem child elements", function() + build.skillsTab.skillSets[1].socketGroupList = { { + enabled = true, + gemList = { { + nameSpec = "Companion: Mighty Silverfist", + level = 20, quality = 0, enabled = true, enableGlobal1 = true, enableGlobal2 = true, + count = 1, corrupted = false, corruptLevel = 0, + fullDPSMinionSkills = { [slamId] = true, [meleeId] = true }, + } }, + } } + + local xml = { } + build.skillsTab:Save(xml) + + local gemNode + for _, skillSetNode in ipairs(xml) do + if skillSetNode.elem == "SkillSet" then + for _, skillNode in ipairs(skillSetNode) do + if skillNode.elem == "Skill" then + gemNode = skillNode[1] + end + end + end + end + assert.is_not_nil(gemNode) + + local skillIds = { } + for _, child in ipairs(gemNode) do + if child.elem == "FullDPSMinionSkill" then + table.insert(skillIds, child.attrib.skillId) + end + end + assert.are.equals(2, #skillIds) + -- sorted for deterministic build XML + assert.are.equals(slamId, skillIds[1]) + assert.are.equals(meleeId, skillIds[2]) + end) + + it("loads FullDPSMinionSkill elements back onto the gem instance", function() + local node = { elem = "Skill", attrib = { enabled = "true" }, + { elem = "Gem", attrib = { nameSpec = "Companion: Mighty Silverfist", level = "20", quality = "0", enabled = "true" }, + { elem = "FullDPSMinionSkill", attrib = { skillId = meleeId } }, + { elem = "FullDPSMinionSkill", attrib = { skillId = slamId } }, + }, + } + + build.skillsTab:LoadSkill(node, 1) + + local socketGroupList = build.skillsTab.skillSets[1].socketGroupList + local gemInstance = socketGroupList[#socketGroupList].gemList[1] + assert.is_not_nil(gemInstance.fullDPSMinionSkills) + assert.True(gemInstance.fullDPSMinionSkills[meleeId]) + assert.True(gemInstance.fullDPSMinionSkills[slamId]) + end) + + it("loads gems without a selection as nil", function() + local node = { elem = "Skill", attrib = { enabled = "true" }, + { elem = "Gem", attrib = { nameSpec = "Fireball", level = "20", quality = "0", enabled = "true" } }, + } + + build.skillsTab:LoadSkill(node, 1) + + local socketGroupList = build.skillsTab.skillSets[1].socketGroupList + assert.is_nil(socketGroupList[#socketGroupList].gemList[1].fullDPSMinionSkills) + end) + end) + + describe("SkillsTab UI", function() + local function displayLastGroup() + local skillsTab = build.skillsTab + skillsTab:SetDisplayGroup(skillsTab.socketGroupList[#skillsTab.socketGroupList]) + skillsTab:UpdateBeastAttackSlots() + return skillsTab + end + + it("creates attack rows with default check state on the active attack", function() + buildCompanionGroup(nil) + local skillsTab = displayLastGroup() + + local attacks = skillsTab:GetDisplayedBeastAttacks() + assert.is_not_nil(attacks) + assert.are.equals(2, #attacks) + assert.is_not_nil(skillsTab.beastAttackSlots[2]) + assert.True(skillsTab.beastAttackSlots[1].enabled.state) + assert.False(skillsTab.beastAttackSlots[2].enabled.state) + end) + + it("shows all attacks unchecked while the group is out of Full DPS", function() + local gemInstance, group = buildCompanionGroup({ [slamId] = true }) + group.includeInFullDPS = false + local skillsTab = displayLastGroup() + + assert.False(skillsTab.beastAttackSlots[1].enabled.state) + assert.False(skillsTab.beastAttackSlots[2].enabled.state) + end) + + it("reflects an explicit selection and replaces the table on toggle", function() + local gemInstance, group = buildCompanionGroup({ [slamId] = true }) + local skillsTab = displayLastGroup() + + assert.False(skillsTab.beastAttackSlots[1].enabled.state) + assert.True(skillsTab.beastAttackSlots[2].enabled.state) + + -- toggling builds a new table (undo snapshots share nested tables) + local before = gemInstance.fullDPSMinionSkills + skillsTab.beastAttackSlots[1].enabled.changeFunc(true) + assert.are_not.equals(before, gemInstance.fullDPSMinionSkills) + assert.True(gemInstance.fullDPSMinionSkills[meleeId]) + assert.True(gemInstance.fullDPSMinionSkills[slamId]) + assert.True(group.includeInFullDPS) + + -- unchecking everything clears the selection and leaves Full DPS, + -- updating the group's checkbox state as well + skillsTab.beastAttackSlots[1].enabled.changeFunc(false) + skillsTab.beastAttackSlots[2].enabled.changeFunc(false) + assert.is_nil(gemInstance.fullDPSMinionSkills) + assert.False(group.includeInFullDPS) + assert.False(skillsTab.controls.includeInFullDPS.state) + end) + + it("checking an attack pulls the group into Full DPS", function() + local gemInstance, group = buildCompanionGroup(nil) + group.includeInFullDPS = false + local skillsTab = displayLastGroup() + + skillsTab.beastAttackSlots[2].enabled.changeFunc(true) + assert.True(group.includeInFullDPS) + assert.True(skillsTab.controls.includeInFullDPS.state) + assert.True(gemInstance.fullDPSMinionSkills[slamId]) + assert.is_nil(gemInstance.fullDPSMinionSkills[meleeId]) + end) + + it("toggling Include in Full DPS syncs the attack selection", function() + local gemInstance, group = buildCompanionGroup({ [slamId] = true }) + local skillsTab = displayLastGroup() + + skillsTab.controls.includeInFullDPS.changeFunc(false) + assert.False(group.includeInFullDPS) + assert.is_nil(gemInstance.fullDPSMinionSkills) + + -- re-enabling selects the first attack + skillsTab.controls.includeInFullDPS.changeFunc(true) + assert.True(group.includeInFullDPS) + assert.True(gemInstance.fullDPSMinionSkills[meleeId]) + assert.is_nil(gemInstance.fullDPSMinionSkills[slamId]) + end) + + it("shows the skill data on hover, gem tooltip style", function() + buildCompanionGroup(nil) + local skillsTab = displayLastGroup() + + local slot = skillsTab.beastAttackSlots[2] + -- plain attack name, no (active) marker, no info button + assert.are.equals("^7Pillar Slam", slot.label.label()) + assert.is_nil(slot.info) + + local tooltip = new("Tooltip") + slot.label.tooltipFunc(tooltip) + local sawTitle, sawDamage, sawTags = false, false, false + for _, line in ipairs(tooltip.lines) do + if line.text and line.text:match("Pillar Slam") then + sawTitle = true + end + -- Pillar Slam's baseMultiplier of 3 renders as "Attack Damage: 300% of base" + if line.text and line.text:match("Attack Damage") and line.text:match("300") then + sawDamage = true + end + -- gem-style tag line from the base flags: attack, melee, area + if line.text and line.text:match("AoE, Attack, Melee") then + sawTags = true + end + end + assert.True(sawTitle) + assert.True(sawDamage) + assert.True(sawTags) + end) + + it("populates the beast dropdown from the build's beast library", function() + buildCompanionGroup(nil) + local skillsTab = displayLastGroup() + + local list = skillsTab.controls.companionBeastSelect.list + assert.are.equals(1, #list) + assert.are.equals(beastId, list[1].minionId) + end) + + it("renames the gem and refreshes the gem slot when the beast changes", function() + local crowbellId = "Metadata/Monsters/CrowBell/CrowBellBossMinion1" + local gemInstance = buildCompanionGroup(nil) + table.insert(build.beastList, crowbellId) + local skillsTab = displayLastGroup() + + skillsTab.controls.companionBeastSelect.selFunc(2, { minionId = crowbellId, label = "The Crowbell" }) + + assert.are.equals(crowbellId, gemInstance.skillMinion) + assert.are.equals("Companion: The Crowbell", gemInstance.nameSpec) + -- the visible gem name box must follow the rename + assert.are.equals("Companion: The Crowbell", skillsTab.gemSlots[1].nameSpec.buf) + end) + end) +end) diff --git a/src/Classes/GemTooltip.lua b/src/Classes/GemTooltip.lua index e72c8634e6..4abfd5bf80 100644 --- a/src/Classes/GemTooltip.lua +++ b/src/Classes/GemTooltip.lua @@ -360,4 +360,75 @@ function GemTooltip.AddGemTooltip(tooltip, build, gemInstance, options) end end +-- Tooltip for a minion's own skill (e.g. a companion attack): the same look as a +-- player gem tooltip, but minion skills carry no gem data, tier or requirements. +-- minionSkill is the live active skill from minion.activeSkillList, whose +-- activeEffect provides the level, quality and actor level for stat evaluation. +function GemTooltip.AddMinionSkillTooltip(tooltip, build, minionSkill) + local fontSizeBig, fontSizeTitle = getFontSizes() + local activeEffect = minionSkill.activeEffect + local grantedEffect = activeEffect.grantedEffect + tooltip.center = false + tooltip.color = colorCodes.GEM + tooltip.minWidth = 400 + tooltip:AddLine(fontSizeTitle, colorCodes.GEM .. grantedEffect.name, "FONTIN SC") + tooltip:AddLine(fontSizeBig, colorCodes.GEMINFO .. "Minion Skill", "FONTIN SC") + -- Gem-style tag line, derived from the stat sets' base flags + local seenTags, tags = { }, { } + for _, statSet in ipairs(grantedEffect.statSets) do + for flag in pairs(statSet.baseFlags or { }) do + if flag ~= "hit" and not seenTags[flag] then + seenTags[flag] = true + table.insert(tags, flag == "area" and "AoE" or (flag:sub(1, 1):upper() .. flag:sub(2))) + end + end + end + table.sort(tags) + if tags[1] then + tooltip:AddLine(fontSizeBig, "^x7F7F7F" .. table.concat(tags, ", "), "FONTIN") + end + tooltip:AddSeparator(8) + local levelStats = grantedEffect.levels[activeEffect.level] or grantedEffect.levels[1] or { } + local isAttack = grantedEffect.skillTypes and grantedEffect.skillTypes[SkillType.Attack] + local cost + for _, res in ipairs(data.costs) do + if levelStats.cost and levelStats.cost[res.Resource] then + cost = (cost and (cost .. ", ") or "") .. res.ResourceString:gsub("{0}", string.format("%g", round(levelStats.cost[res.Resource] / res.Divisor, 2))) + end + end + if cost then + tooltip:AddLine(fontSizeBig, colorCodes.GEMINFO .. "Cost: ^7" .. cost, "FONTIN SC") + end + if levelStats.cooldown then + local line = colorCodes.GEMINFO .. string.format("Cooldown Time: ^7%.2f sec", levelStats.cooldown) + if levelStats.storedUses and levelStats.storedUses > 1 then + line = line .. string.format(" (%d uses)", levelStats.storedUses) + end + tooltip:AddLine(fontSizeBig, line, "FONTIN SC") + end + if isAttack then + if levelStats.attackSpeedMultiplier then + tooltip:AddLine(fontSizeBig, colorCodes.GEMINFO .. string.format("Attack Speed: ^7%d%% of base", levelStats.attackSpeedMultiplier + 100), "FONTIN SC") + end + if levelStats.attackTime then + tooltip:AddLine(fontSizeBig, colorCodes.GEMINFO .. string.format("Attack Time: ^7%.2f sec", levelStats.attackTime / 1000), "FONTIN SC") + end + if levelStats.baseMultiplier then + tooltip:AddLine(fontSizeBig, colorCodes.GEMINFO .. string.format("Attack Damage: ^7%g%% of base", levelStats.baseMultiplier * 100), "FONTIN SC") + end + elseif (grantedEffect.castTime or 0) > 0 then + tooltip:AddLine(fontSizeBig, colorCodes.GEMINFO .. string.format("Cast Time: ^7%.2f sec", grantedEffect.castTime), "FONTIN SC") + end + if levelStats.critChance then + tooltip:AddLine(fontSizeBig, colorCodes.GEMINFO .. string.format("Critical Hit Chance: ^7%.2f%%", levelStats.critChance), "FONTIN SC") + end + if grantedEffect.description then + tooltip:AddSeparator(10) + for _, line in ipairs(main:WrapString(grantedEffect.description, 16, 400)) do + tooltip:AddLine(fontSizeBig, colorCodes.GEMDESCRIPTION .. line, "FONTIN ITALIC") + end + end + addEffectStats(tooltip, build, activeEffect, grantedEffect, nil, nil) +end + return GemTooltip diff --git a/src/Classes/LabelControl.lua b/src/Classes/LabelControl.lua index 2f799ece2d..311bf5f99e 100644 --- a/src/Classes/LabelControl.lua +++ b/src/Classes/LabelControl.lua @@ -3,15 +3,31 @@ -- Class: Label Control -- Simple text label. -- -local LabelClass = newClass("LabelControl", "Control", function(self, anchor, rect, label) +local LabelClass = newClass("LabelControl", "Control", "TooltipHost", function(self, anchor, rect, label) self.Control(anchor, rect) + self.TooltipHost() self.label = label self.width = function() return DrawStringWidth(self:GetProperty("height"), "VAR", self:GetProperty("label")) end end) -function LabelClass:Draw() +-- Labels are mouse-transparent unless a tooltip has been set on them, so they +-- never intercept hover or clicks meant for surrounding controls +function LabelClass:IsMouseOver() + if not self:IsShown() or not (self.tooltipText or self.tooltipFunc) then + return false + end + return self:IsMouseInBounds() +end + +function LabelClass:Draw(viewPort) local x, y = self:GetPos() DrawString(x, y, "LEFT", self:GetProperty("height"), "VAR", self:GetProperty("label")) -end \ No newline at end of file + if self:IsMouseOver() then + SetDrawLayer(nil, 100) + local width, height = self:GetSize() + self:DrawTooltip(x, y, width, height, viewPort) + SetDrawLayer(nil, 0) + end +end diff --git a/src/Classes/SkillListControl.lua b/src/Classes/SkillListControl.lua index 891f4374b4..069360ecd9 100644 --- a/src/Classes/SkillListControl.lua +++ b/src/Classes/SkillListControl.lua @@ -178,6 +178,7 @@ function SkillListClass:OnHoverKeyUp(key) elseif key == "RIGHTBUTTON" then if IsKeyDown("CTRL") then item.includeInFullDPS = not item.includeInFullDPS + self.skillsTab:SyncCompanionFullDPS(item) if item == self.skillsTab.displayGroup then self.skillsTab:SetDisplayGroup(item) end diff --git a/src/Classes/SkillsTab.lua b/src/Classes/SkillsTab.lua index 288df43bc5..5749f8e256 100644 --- a/src/Classes/SkillsTab.lua +++ b/src/Classes/SkillsTab.lua @@ -10,6 +10,8 @@ local t_remove = table.remove local m_min = math.min local m_max = math.max +local gemTooltip = LoadModule("Classes/GemTooltip") + local groupSlotDropList = { { label = "None" }, { label = "Weapon 1", slotName = "Weapon 1" }, @@ -212,6 +214,7 @@ local SkillsTabClass = newClass("SkillsTab", "UndoHandler", "ControlHost", "Cont end self.controls.includeInFullDPS = new("CheckBoxControl", { "LEFT", self.controls.groupEnabled, "RIGHT" }, { 145, 0, 20 }, "Include in Full DPS:", function(state) self.displayGroup.includeInFullDPS = state + self:SyncCompanionFullDPS(self.displayGroup) self:AddUndoState() self.build.buildFlag = true end) @@ -283,9 +286,9 @@ will automatically apply to the skill.]] self.controls.gemEnableHeader = new("LabelControl", {"BOTTOMLEFT", self.gemSlots[1].enabled, "TOPLEFT"}, {-16, -2, 0, 16}, "^7Enabled:") self.controls.gemCountHeader = new("LabelControl", {"BOTTOMLEFT", self.gemSlots[1].count, "TOPLEFT"}, {18, -2, 0, 16}, "^7Count:") - -- Tamed beast (companion) modifiers - self.anchorBeastMods = new("Control", nil, {0, 0, 0, 0}) - self.anchorBeastMods:SetAnchor("TOPLEFT", self.anchorGemSlots, "TOPLEFT", 0, function() + -- Tamed beast (companion) selection and Full DPS attack list + self.anchorCompanion = new("Control", nil, {0, 0, 0, 0}) + self.anchorCompanion:SetAnchor("TOPLEFT", self.anchorGemSlots, "TOPLEFT", 0, function() local y = 0 for i = 1, (self.displayGroup and #self.displayGroup.gemList or 0) + 1 do local slot = self.gemSlots[i] @@ -293,6 +296,55 @@ will automatically apply to the skill.]] end return y + 14 end) + local function companionShown() + return self:GetDisplayedBeastGem() ~= nil + end + self.controls.companionBeastLabel = new("LabelControl", {"TOPLEFT", self.anchorCompanion, "TOPLEFT"}, {0, 20, 0, 16}, "^7Beast:") + self.controls.companionBeastLabel.shown = companionShown + self.controls.companionBeastSelect = new("DropDownControl", {"LEFT", self.controls.companionBeastLabel, "RIGHT"}, {4, 0, 250, 18}, nil, function(index, value) + local gemInstance = self:GetDisplayedBeastGem() + if gemInstance and value.minionId then + gemInstance.skillMinion = value.minionId + gemInstance.skillMinionCalcs = value.minionId + -- Gems resolved by id keep a "Companion: " display name; nameSpec-resolved + -- gems must keep their resolvable name (see ProcessSocketGroup) + if gemInstance.gemId and gemInstance.nameSpec and gemInstance.nameSpec:match("^Companion:") then + gemInstance.nameSpec = "Companion: "..value.label + end + -- gem slot text is only written by SetDisplayGroup, so refresh it to show the rename + self:SetDisplayGroup(self.displayGroup) + self:AddUndoState() + self.build.modFlag = true + self.build.buildFlag = true + end + end) + self.controls.companionBeastSelect.shown = companionShown + self.controls.companionBeastManage = new("ButtonControl", {"LEFT", self.controls.companionBeastSelect, "RIGHT"}, {8, 0, 110, 18}, "Manage Beasts...", function() + self.build:OpenSpectreLibrary("beast") + end) + self.controls.companionBeastManage.shown = companionShown + self.beastAttackSlots = { } + self:CreateBeastAttackSlot(1) + self.controls.beastAttackHeader = new("LabelControl", {"BOTTOMLEFT", self.beastAttackSlots[1].enabled, "TOPLEFT"}, {0, -2, 0, 16}, "^7Include in Full DPS:") + self.controls.beastAttackHeader.shown = function() + return self:GetDisplayedBeastAttacks() ~= nil + end + self.controls.beastAttackHeader.tooltipText = "Checked attacks each count as their own entry in Full DPS.\nChecking an attack includes the group in Full DPS; unchecking all attacks removes it." + + -- Tamed beast (companion) modifiers + self.anchorBeastMods = new("Control", nil, {0, 0, 0, 0}) + self.anchorBeastMods:SetAnchor("TOPLEFT", self.anchorCompanion, "TOPLEFT", 0, function() + if not self:GetDisplayedBeastGem() then + return 0 + end + local attacks = self:GetDisplayedBeastAttacks() + if not attacks then + -- beast row only (no calc data for the attack list yet) + return 44 + end + -- beast row + attacks header + one row per attack + return 62 + #attacks * 20 + 6 + end) self.beastModSlots = { } self:CreateBeastModSlot(1) self.controls.beastModHeader = new("LabelControl", {"BOTTOMLEFT", self.beastModSlots[1].select, "TOPLEFT"}, {0, -2, 0, 16}, "^7Beast Modifiers:") @@ -414,6 +466,9 @@ function SkillsTabClass:LoadSkill(node, skillSetId) display = child.attrib.display, enabled = child.attrib.enabled == "true", }) + elseif child.elem == "FullDPSMinionSkill" and child.attrib.skillId then + gemInstance.fullDPSMinionSkills = gemInstance.fullDPSMinionSkills or { } + gemInstance.fullDPSMinionSkills[child.attrib.skillId] = true end end @@ -574,6 +629,17 @@ function SkillsTabClass:Save(xml) } } ) end end + if gemInstance.fullDPSMinionSkills then + -- sorted for deterministic build XML + local skillIds = { } + for skillId in pairs(gemInstance.fullDPSMinionSkills) do + t_insert(skillIds, skillId) + end + table.sort(skillIds) + for _, skillId in ipairs(skillIds) do + t_insert(gemInfo, { elem = "FullDPSMinionSkill", attrib = { skillId = skillId } } ) + end + end t_insert(node, gemInfo) end t_insert(child, node) @@ -640,6 +706,7 @@ function SkillsTabClass:Draw(viewPort, inputEvents) end self:UpdateGemSlots() + self:UpdateBeastAttackSlots() self:UpdateBeastModSlots() self:DrawControls(viewPort) @@ -1140,12 +1207,12 @@ function SkillsTabClass:CreateGemSlot(index) self.controls["gemSlot"..index.."EnableGlobal2"] = slot.enableGlobal2 end --- Returns the displayed group's Companion gem, if any (only companions carry tamed beast mods) -function SkillsTabClass:GetDisplayedBeastGem() - if not self.displayGroup then +-- Returns the group's Companion gem, if any (only companions carry tamed beast mods) +function SkillsTabClass:GetBeastGemForGroup(socketGroup) + if not socketGroup then return end - for _, gemInstance in ipairs(self.displayGroup.gemList) do + for _, gemInstance in ipairs(socketGroup.gemList) do local grantedEffect = gemInstance.gemData and gemInstance.gemData.grantedEffect if grantedEffect and grantedEffect.minionList and grantedEffect.name:match("^Companion") then return gemInstance @@ -1153,6 +1220,180 @@ function SkillsTabClass:GetDisplayedBeastGem() end end +function SkillsTabClass:GetDisplayedBeastGem() + return self:GetBeastGemForGroup(self.displayGroup) +end + +-- Returns the group companion's attack list (minion.activeSkillList), its owning +-- active skill and the gem instance; the list is nil when no calc data is available +-- (group disabled, no beast, or no calc pass since the last edit) +function SkillsTabClass:GetBeastAttacksForGroup(socketGroup) + local gemInstance = self:GetBeastGemForGroup(socketGroup) + if not gemInstance or not socketGroup.displaySkillList then + return nil, nil, gemInstance + end + for _, activeSkill in ipairs(socketGroup.displaySkillList) do + if activeSkill.activeEffect.srcInstance == gemInstance and activeSkill.minion and activeSkill.minion.activeSkillList then + return activeSkill.minion.activeSkillList, activeSkill, gemInstance + end + end + return nil, nil, gemInstance +end + +function SkillsTabClass:GetDisplayedBeastAttacks() + return self:GetBeastAttacksForGroup(self.displayGroup) +end + +-- Keep the companion attack selection in step with the group's Include in Full DPS +-- state: enabling it selects the first attack, disabling it clears the selection +function SkillsTabClass:SyncCompanionFullDPS(socketGroup) + local attacks, _, gemInstance = self:GetBeastAttacksForGroup(socketGroup) + if not gemInstance then + return + end + if not socketGroup.includeInFullDPS then + gemInstance.fullDPSMinionSkills = nil + elseif attacks and attacks[1] then + gemInstance.fullDPSMinionSkills = { [attacks[1].activeEffect.grantedEffect.id] = true } + end +end + +-- The active attack: the one the sidebar's minion skill dropdown selects, which is +-- what counts for Full DPS when no explicit per-attack selection has been made +function SkillsTabClass:GetDisplayedBeastActiveAttackIndex(gemInstance, attacks) + return m_max(m_min(gemInstance.skillMinionSkill or 1, #attacks), 1) +end + +function SkillsTabClass:CreateBeastAttackSlot(index) + local slot = { } + self.beastAttackSlots[index] = slot + + local function isRowShown() + local attacks = self:GetDisplayedBeastAttacks() + return attacks ~= nil and index <= #attacks + end + + -- Include this attack in Full DPS + slot.enabled = new("CheckBoxControl", nil, {0, 0, 18}, nil, function(state) + local attacks, activeSkill, gemInstance = self:GetDisplayedBeastAttacks() + local minionSkill = attacks and attacks[index] + if not minionSkill then + return + end + -- Build a new table every time: undo snapshots share nested tables (shallow + -- copyTable in CreateUndoState), so in-place mutation would corrupt them + local newSelection = { } + if gemInstance.fullDPSMinionSkills then + for skillId, value in pairs(gemInstance.fullDPSMinionSkills) do + newSelection[skillId] = value + end + elseif self.displayGroup.includeInFullDPS then + -- Materialize the legacy default: only the active attack counts + local activeIndex = self:GetDisplayedBeastActiveAttackIndex(gemInstance, attacks) + newSelection[attacks[activeIndex].activeEffect.grantedEffect.id] = true + end + newSelection[minionSkill.activeEffect.grantedEffect.id] = state or nil + -- The selection and the group's Include in Full DPS state move together: + -- checking any attack turns it on, unchecking the last one turns it off + if next(newSelection) then + gemInstance.fullDPSMinionSkills = newSelection + self.displayGroup.includeInFullDPS = true + else + gemInstance.fullDPSMinionSkills = nil + self.displayGroup.includeInFullDPS = false + end + -- the group checkbox state is only refreshed by SetDisplayGroup, so mirror it here + self.controls.includeInFullDPS.state = self.displayGroup.includeInFullDPS and self.displayGroup.enabled + self:AddUndoState() + self.build.buildFlag = true + end) + if index == 1 then + slot.enabled:SetAnchor("TOPLEFT", self.anchorCompanion, "TOPLEFT", 0, 62) + else + slot.enabled:SetAnchor("TOPLEFT", self.beastAttackSlots[index - 1].enabled, "BOTTOMLEFT", 0, 2) + end + slot.enabled.shown = isRowShown + slot.enabled.tooltipText = "Include this attack as its own Full DPS entry." + self.controls["beastAttackSlot"..index.."Enable"] = slot.enabled + + -- Attack name; hovering it shows the skill's data, gem tooltip style + slot.label = new("LabelControl", {"LEFT", slot.enabled, "RIGHT"}, {6, 0, 0, 16}, function() + local attacks = self:GetDisplayedBeastAttacks() + local minionSkill = attacks and attacks[index] + if not minionSkill then + return "" + end + return "^7"..minionSkill.activeEffect.grantedEffect.name + end) + slot.label.shown = isRowShown + slot.label.tooltipFunc = function(tooltip) + local attacks = self:GetDisplayedBeastAttacks() + local minionSkill = attacks and attacks[index] + if tooltip:CheckForUpdate(self.build.outputRevision, self.displayGroup, minionSkill) and minionSkill then + gemTooltip.AddMinionSkillTooltip(tooltip, self.build, minionSkill) + end + end + self.controls["beastAttackSlot"..index.."Label"] = slot.label +end + +-- Update the companion controls to reflect the currently displayed socket group +function SkillsTabClass:UpdateBeastAttackSlots() + local gemInstance = self:GetDisplayedBeastGem() + if not gemInstance then + return + end + -- Beast selection list, sourced from the build's tamed beast library + wipeTable(self.controls.companionBeastSelect.list) + for _, minionId in ipairs(self.build.beastList or { }) do + local minion = self.build.data.minions[minionId] + if minion then + t_insert(self.controls.companionBeastSelect.list, { label = minion.name, minionId = minionId }) + end + end + if not self.controls.companionBeastSelect.list[1] then + t_insert(self.controls.companionBeastSelect.list, { label = "" }) + end + self.controls.companionBeastSelect:SelByValue(gemInstance.skillMinion, "minionId") + self.controls.companionBeastSelect.enabled = self.controls.companionBeastSelect.list[1].minionId ~= nil + -- Attack rows: create on demand, like gem and beast mod slots + local attacks = self:GetDisplayedBeastAttacks() + for index = 1, (attacks and #attacks or 0) do + if not self.beastAttackSlots[index] then + self:CreateBeastAttackSlot(index) + end + end + if not attacks then + return + end + -- Checkbox states: nothing counts while the group is out of Full DPS; with it + -- on, the explicit selection governs when any of its ids matches this beast's + -- attacks, otherwise the legacy default (only the active attack counts) + local includeInFullDPS = self.displayGroup and self.displayGroup.includeInFullDPS + local selection = gemInstance.fullDPSMinionSkills + local anyMatch = false + if selection then + for _, minionSkill in ipairs(attacks) do + if selection[minionSkill.activeEffect.grantedEffect.id] then + anyMatch = true + break + end + end + end + local activeIndex = self:GetDisplayedBeastActiveAttackIndex(gemInstance, attacks) + for index, slot in ipairs(self.beastAttackSlots) do + local minionSkill = attacks[index] + if minionSkill then + if not includeInFullDPS then + slot.enabled.state = false + elseif anyMatch then + slot.enabled.state = selection[minionSkill.activeEffect.grantedEffect.id] or false + else + slot.enabled.state = index == activeIndex + end + end + end +end + function SkillsTabClass:GetBeastModDropList() if not self.beastModDropList then local sorted = { } diff --git a/src/Modules/Build.lua b/src/Modules/Build.lua index c5dd0b6c03..cfa8625265 100644 --- a/src/Modules/Build.lua +++ b/src/Modules/Build.lua @@ -431,6 +431,11 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin srcInstance.nameSpec = "Companion: ".. value.label end end + -- the Skills tab gem slot text is only written by SetDisplayGroup; refresh it + -- if the renamed gem's group is the one on display there + if self.skillsTab.displayGroup == mainSocketGroup then + self.skillsTab:SetDisplayGroup(mainSocketGroup) + end self.modFlag = true self.buildFlag = true end) diff --git a/src/Modules/CalcActiveSkill.lua b/src/Modules/CalcActiveSkill.lua index 079f360497..c045dcc4c4 100644 --- a/src/Modules/CalcActiveSkill.lua +++ b/src/Modules/CalcActiveSkill.lua @@ -1105,7 +1105,12 @@ function calcs.createMinionSkills(env, activeSkill) t_insert(minion.activeSkillList, minionSkill) end local skillIndex - if env.mode == "CALCS" then + if activeSkill.minionSkillIndexOverride then + -- Transient override used by calcFullDPS to evaluate each selected companion + -- attack in turn; never written back to srcInstance so saved builds and the + -- sidebar's active attack selection are unaffected + skillIndex = m_max(m_min(activeSkill.minionSkillIndexOverride, #minion.activeSkillList), 1) + elseif env.mode == "CALCS" then skillIndex = m_max(m_min(activeEffect.srcInstance.skillMinionSkillCalcs or 1, #minion.activeSkillList), 1) activeEffect.srcInstance.skillMinionSkillCalcs = skillIndex else diff --git a/src/Modules/Calcs.lua b/src/Modules/Calcs.lua index 7f06f8bd6d..2572af98a1 100644 --- a/src/Modules/Calcs.lua +++ b/src/Modules/Calcs.lua @@ -176,6 +176,56 @@ function calcs.calcFullDPS(build, mode, override, specEnv) local burningGroundSource = "" local causticGroundSource = "" + -- Re-Build env calculator for new run + local function reinitEnv() + local accelerationTbl = { + nodeAlloc = true, + requirementsItems = true, + requirementsGems = true, + skills = true, + everything = true, + } + fullEnv, _, _, _ = calcs.initEnv(build, mode, override, { cachedPlayerDB = cachedPlayerDB, cachedEnemyDB = cachedEnemyDB, cachedMinionDB = cachedMinionDB, env = fullEnv, accelerate = accelerationTbl }) + end + + -- Collect the minion's output of one calc pass into the Full DPS totals; + -- returns the minion name prefix when the minion contributed hit DPS + local function harvestMinionOutput(usedEnv, activeSkill, skillName, activeSkillCount) + local minionName = nil + if usedEnv.minion.output.TotalDPS and usedEnv.minion.output.TotalDPS > 0 then + minionName = (activeSkill.minion and activeSkill.minion.minionData.name..": ") or (usedEnv.minion and usedEnv.minion.minionData.name..": ") or "" + -- Minion attacks list as their own entries: the attack name leads and the + -- owning skill is shown as the source ("from Companion: ") + t_insert(fullDPS.skills, { name = activeSkill.skillPartName, dps = usedEnv.minion.output.TotalDPS, count = activeSkillCount, trigger = activeSkill.infoTrigger, source = skillName }) + fullDPS.combinedDPS = fullDPS.combinedDPS + usedEnv.minion.output.TotalDPS * activeSkillCount + end + if usedEnv.minion.output.BleedDPS and usedEnv.minion.output.BleedDPS > fullDPS.bleedDPS then + fullDPS.bleedDPS = usedEnv.minion.output.BleedDPS + bleedSource = skillName + end + if usedEnv.minion.output.IgniteDPS and usedEnv.minion.output.IgniteDPS > fullDPS.igniteDPS then + fullDPS.igniteDPS = usedEnv.minion.output.IgniteDPS + igniteSource = skillName + end + if usedEnv.minion.output.PoisonDPS and usedEnv.minion.output.PoisonDPS > fullDPS.poisonDPS then + fullDPS.poisonDPS = usedEnv.minion.output.PoisonDPS + poisonSource = skillName + end + if usedEnv.minion.output.ImpaleDPS and usedEnv.minion.output.ImpaleDPS > 0 then + fullDPS.impaleDPS = fullDPS.impaleDPS + usedEnv.minion.output.ImpaleDPS * activeSkillCount + end + if usedEnv.minion.output.DecayDPS and usedEnv.minion.output.DecayDPS > 0 then + fullDPS.decayDPS = fullDPS.decayDPS + usedEnv.minion.output.DecayDPS + end + if usedEnv.minion.output.TotalDot and usedEnv.minion.output.TotalDot > 0 then + fullDPS.dotDPS = fullDPS.dotDPS + usedEnv.minion.output.TotalDot + end + if usedEnv.minion.output.CullMultiplier and usedEnv.minion.output.CullMultiplier > 1 and usedEnv.minion.output.CullMultiplier > fullDPS.cullingMulti then + fullDPS.cullingMulti = usedEnv.minion.output.CullMultiplier + end + return minionName + end + for _, activeSkill in ipairs(fullEnv.player.activeSkillList) do if activeSkill.socketGroup and activeSkill.socketGroup.includeInFullDPS then local activeSkillCount, enabled = calcs.getActiveSkillCount(activeSkill) @@ -183,36 +233,44 @@ function calcs.calcFullDPS(build, mode, override, specEnv) fullEnv.player.mainSkill = activeSkill calcs.perform(fullEnv, true) usedEnv = fullEnv - local minionName = nil - if activeSkill.minion or usedEnv.minion then - if usedEnv.minion.output.TotalDPS and usedEnv.minion.output.TotalDPS > 0 then - minionName = (activeSkill.minion and activeSkill.minion.minionData.name..": ") or (usedEnv.minion and usedEnv.minion.minionData.name..": ") or "" - t_insert(fullDPS.skills, { name = activeSkill.activeEffect.grantedEffect.name, dps = usedEnv.minion.output.TotalDPS, count = activeSkillCount, trigger = activeSkill.infoTrigger, skillPart = minionName..activeSkill.skillPartName }) - fullDPS.combinedDPS = fullDPS.combinedDPS + usedEnv.minion.output.TotalDPS * activeSkillCount - end - if usedEnv.minion.output.BleedDPS and usedEnv.minion.output.BleedDPS > fullDPS.bleedDPS then - fullDPS.bleedDPS = usedEnv.minion.output.BleedDPS - bleedSource = activeSkill.activeEffect.grantedEffect.name - end - if usedEnv.minion.output.IgniteDPS and usedEnv.minion.output.IgniteDPS > fullDPS.igniteDPS then - fullDPS.igniteDPS = usedEnv.minion.output.IgniteDPS - igniteSource = activeSkill.activeEffect.grantedEffect.name - end - if usedEnv.minion.output.PoisonDPS and usedEnv.minion.output.PoisonDPS > fullDPS.poisonDPS then - fullDPS.poisonDPS = usedEnv.minion.output.PoisonDPS - poisonSource = activeSkill.activeEffect.grantedEffect.name + -- Companion/Spectre gems all share a single granted effect ("Companion: {0}"/"Spectre: {0}"), + -- whose name is mutated globally for display, so derive the entry name from this skill's own minion + local skillName = activeSkill.activeEffect.grantedEffect.name + local skillMinion = activeSkill.minion or usedEnv.minion + if skillMinion and skillMinion.minionData then + if skillName:match("^Companion:") then + skillName = "Companion: "..skillMinion.minionData.name + elseif skillName:match("^Spectre:") then + skillName = "Spectre: "..skillMinion.minionData.name end - if usedEnv.minion.output.ImpaleDPS and usedEnv.minion.output.ImpaleDPS > 0 then - fullDPS.impaleDPS = fullDPS.impaleDPS + usedEnv.minion.output.ImpaleDPS * activeSkillCount - end - if usedEnv.minion.output.DecayDPS and usedEnv.minion.output.DecayDPS > 0 then - fullDPS.decayDPS = fullDPS.decayDPS + usedEnv.minion.output.DecayDPS + end + -- Resolve the companion attack multi-select: each selected attack is + -- evaluated as its own Full DPS entry; the active attack only counts + -- if selected. Ids that don't match this beast's attacks are stale + -- (e.g. the beast was swapped) and leave the default behaviour. + local extraPasses = { } + local harvestPass1 = true + local selection = activeSkill.activeEffect.srcInstance and activeSkill.activeEffect.srcInstance.fullDPSMinionSkills + local minionSkills = activeSkill.minion and activeSkill.minion.activeSkillList + if selection and minionSkills and skillName:match("^Companion") then + local activeIndex = isValueInArray(minionSkills, activeSkill.minion.mainSkill) + local anyMatch = false + for i, minionSkill in ipairs(minionSkills) do + if selection[minionSkill.activeEffect.grantedEffect.id] then + anyMatch = true + if i ~= activeIndex then + t_insert(extraPasses, i) + end + end end - if usedEnv.minion.output.TotalDot and usedEnv.minion.output.TotalDot > 0 then - fullDPS.dotDPS = fullDPS.dotDPS + usedEnv.minion.output.TotalDot + if anyMatch then + harvestPass1 = activeIndex and selection[minionSkills[activeIndex].activeEffect.grantedEffect.id] or false end - if usedEnv.minion.output.CullMultiplier and usedEnv.minion.output.CullMultiplier > 1 and usedEnv.minion.output.CullMultiplier > fullDPS.cullingMulti then - fullDPS.cullingMulti = usedEnv.minion.output.CullMultiplier + end + local minionName = nil + if activeSkill.minion or usedEnv.minion then + if harvestPass1 then + minionName = harvestMinionOutput(usedEnv, activeSkill, skillName, activeSkillCount) end -- This is a fix to prevent Absolution spell hit from being counted multiple times when increasing minions count if activeSkill.activeEffect.grantedEffect.name == "Absolution" and fullEnv.modDB:Flag(false, "Condition:AbsolutionSkillDamageCountedOnce") then @@ -304,15 +362,20 @@ function calcs.calcFullDPS(build, mode, override, specEnv) fullDPS.cullingMulti = usedEnv.player.output.CullMultiplier end - -- Re-Build env calculator for new run - local accelerationTbl = { - nodeAlloc = true, - requirementsItems = true, - requirementsGems = true, - skills = true, - everything = true, - } - fullEnv, _, _, _ = calcs.initEnv(build, mode, override, { cachedPlayerDB = cachedPlayerDB, cachedEnemyDB = cachedEnemyDB, cachedMinionDB = cachedMinionDB, env = fullEnv, accelerate = accelerationTbl }) + reinitEnv() + + -- Evaluate the remaining selected companion attacks, one Full DPS entry each + for _, attackIndex in ipairs(extraPasses) do + activeSkill.minionSkillIndexOverride = attackIndex + fullEnv.player.mainSkill = activeSkill + calcs.perform(fullEnv, true) + usedEnv = fullEnv + if usedEnv.minion then + harvestMinionOutput(usedEnv, activeSkill, skillName, activeSkillCount) + end + reinitEnv() + end + activeSkill.minionSkillIndexOverride = nil end end end