SFrames.MinimapBuffs = {} local MB = SFrames.MinimapBuffs local MAX_BUFFS = 32 local MAX_DEBUFFS = 16 local UPDATE_INTERVAL = 0.2 local BASE_X = -209 local BASE_Y = -26 local ROUND_BACKDROP = { bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 8, edgeSize = 8, insets = { left = 2, right = 2, top = 2, bottom = 2 }, } local function GetDB() if not SFramesDB or type(SFramesDB.MinimapBuffs) ~= "table" then return { enabled = true, iconSize = 30, iconsPerRow = 8, spacing = 2, growDirection = "LEFT", position = "TOPRIGHT", offsetX = 0, offsetY = 0, showTimer = true, showDebuffs = true, debuffIconSize = 30, } end return SFramesDB.MinimapBuffs end local function FormatBuffTime(seconds) if not seconds or seconds <= 0 or seconds >= 99999 then return "N/A" end if seconds >= 3600 then local h = math.floor(seconds / 3600) local m = math.floor(math.mod(seconds, 3600) / 60) return h .. "h" .. m .. "m" elseif seconds >= 60 then return math.floor(seconds / 60) .. "m" else return math.floor(seconds) .. "s" end end local DEBUFF_TYPE_COLORS = { Magic = { r = 0.20, g = 0.60, b = 1.00 }, Curse = { r = 0.60, g = 0.00, b = 1.00 }, Disease = { r = 0.60, g = 0.40, b = 0.00 }, Poison = { r = 0.00, g = 0.60, b = 0.00 }, } local DEBUFF_DEFAULT_COLOR = { r = 0.80, g = 0.00, b = 0.00 } local WEAPON_ENCHANT_COLOR = { r = 0.58, g = 0.22, b = 0.82 } local function HideBlizzardBuffs() for i = 0, 23 do local btn = _G["BuffButton" .. i] if btn then btn:Hide() btn:UnregisterAllEvents() btn.Show = function() end end end for i = 1, 3 do local te = _G["TempEnchant" .. i] if te then te:Hide() te:UnregisterAllEvents() te.Show = function() end end end if BuffFrame then BuffFrame:Hide() BuffFrame:UnregisterAllEvents() BuffFrame.Show = function() end end if TemporaryEnchantFrame then TemporaryEnchantFrame:Hide() TemporaryEnchantFrame:UnregisterAllEvents() TemporaryEnchantFrame.Show = function() end end end local function ApplySlotBackdrop(btn, isBuff) btn:SetBackdrop(ROUND_BACKDROP) btn:SetBackdropColor(0.06, 0.06, 0.08, 0.92) if isBuff then btn:SetBackdropBorderColor(0.25, 0.25, 0.30, 1) else local c = DEBUFF_DEFAULT_COLOR btn:SetBackdropBorderColor(c.r, c.g, c.b, 1) end end local function CreateSlot(parent, namePrefix, index, isBuff) local db = GetDB() local size = isBuff and (db.iconSize or 30) or (db.debuffIconSize or 30) local btn = CreateFrame("Button", namePrefix .. index, parent) btn:SetWidth(size) btn:SetHeight(size) ApplySlotBackdrop(btn, isBuff) btn.icon = btn:CreateTexture(nil, "ARTWORK") btn.icon:SetPoint("TOPLEFT", btn, "TOPLEFT", 2, -2) btn.icon:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -2, 2) btn.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93) btn.count = SFrames:CreateFontString(btn, 10, "RIGHT") btn.count:SetPoint("TOPRIGHT", btn, "TOPRIGHT", -1, -1) btn.count:SetTextColor(1, 1, 1) btn.count:SetShadowColor(0, 0, 0, 1) btn.count:SetShadowOffset(1, -1) btn.timer = SFrames:CreateFontString(btn, 9, "CENTER") btn.timer:SetPoint("BOTTOM", btn, "BOTTOM", 0, -11) btn.timer:SetTextColor(1, 0.82, 0) btn.timer:SetShadowColor(0, 0, 0, 1) btn.timer:SetShadowOffset(1, -1) btn.isBuff = isBuff btn:EnableMouse(true) btn:RegisterForClicks("RightButtonUp") btn:SetScript("OnEnter", function() if this._sfSimulated then GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT") GameTooltip:AddLine(this._sfSimLabel or "Simulated", 1, 1, 1) GameTooltip:AddLine(this._sfSimDesc or "", 0.7, 0.7, 0.7) GameTooltip:Show() return end if this._isWeaponEnchant and this._weaponSlotID then GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT") GameTooltip:SetInventoryItem("player", this._weaponSlotID) return end if this.buffIndex and this.buffIndex >= 0 then GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT") GameTooltip:SetPlayerBuff(this.buffIndex) end end) btn:SetScript("OnLeave", function() GameTooltip:Hide() end) btn:SetScript("OnClick", function() if this._sfSimulated then return end if this._isWeaponEnchant then return end if this.isBuff and this.buffIndex and this.buffIndex >= 0 then CancelPlayerBuff(this.buffIndex) end end) btn.buffIndex = -1 btn._sfSimulated = false btn._isWeaponEnchant = false btn._weaponSlotID = nil btn:Hide() return btn end function MB:ApplyLayout() if not self.buffSlots then return end local db = GetDB() local size = db.iconSize or 30 local spacing = db.spacing or 2 local perRow = db.iconsPerRow or 8 local growLeft = (db.growDirection == "LEFT") for i = 1, MAX_BUFFS do local btn = self.buffSlots[i] btn:SetWidth(size) btn:SetHeight(size) btn:ClearAllPoints() local col = math.mod(i - 1, perRow) local row = math.floor((i - 1) / perRow) local xDir = growLeft and -1 or 1 local xOfs = col * (size + spacing) * xDir local yOfs = -row * (size + 14 + spacing) local anchor = growLeft and "TOPRIGHT" or "TOPLEFT" btn:SetPoint(anchor, self.buffContainer, anchor, xOfs, yOfs) end if not self.debuffSlots then return end local dSize = db.debuffIconSize or 30 for i = 1, MAX_DEBUFFS do local btn = self.debuffSlots[i] btn:SetWidth(dSize) btn:SetHeight(dSize) btn:ClearAllPoints() local col = math.mod(i - 1, perRow) local row = math.floor((i - 1) / perRow) local xDir = growLeft and -1 or 1 local xOfs = col * (dSize + spacing) * xDir local yOfs = -row * (dSize + 14 + spacing) local anchor = growLeft and "TOPRIGHT" or "TOPLEFT" btn:SetPoint(anchor, self.debuffContainer, anchor, xOfs, yOfs) end end local function CountVisibleRows(slots, maxSlots, perRow) local maxVisible = 0 for i = 1, maxSlots do if slots[i]:IsShown() then maxVisible = i end end if maxVisible == 0 then return 0 end return math.floor((maxVisible - 1) / perRow) + 1 end function MB:ApplyPosition() if not self.buffContainer then return end local db = GetDB() local pos = db.position or "TOPRIGHT" local x = BASE_X + (db.offsetX or 0) local y = BASE_Y + (db.offsetY or 0) self.buffContainer:ClearAllPoints() self.buffContainer:SetPoint(pos, UIParent, pos, x, y) self:AnchorDebuffs() end function MB:AnchorDebuffs() if not self.debuffContainer or not self.buffContainer then return end local db = GetDB() local size = db.iconSize or 30 local spacing = db.spacing or 2 local perRow = db.iconsPerRow or 8 local rows = CountVisibleRows(self.buffSlots, MAX_BUFFS, perRow) local rowHeight = size + 14 + spacing local gap = 6 self.debuffContainer:ClearAllPoints() self.debuffContainer:SetPoint("TOPRIGHT", self.buffContainer, "TOPRIGHT", 0, -(rows * rowHeight + gap)) end local function ApplyTimerColor(btn, timeText) if timeText == "N/A" then btn.timer:SetTextColor(0.6, 0.6, 0.6) return end local secs = nil local _, _, hVal = string.find(timeText, "(%d+)h") local _, _, mVal = string.find(timeText, "(%d+)m") local _, _, sVal = string.find(timeText, "(%d+)s") local h = tonumber(hVal) local m = tonumber(mVal) local s = tonumber(sVal) if h then secs = h * 3600 + (m or 0) * 60 elseif m then secs = m * 60 elseif s then secs = s end if secs and secs < 30 then btn.timer:SetTextColor(1, 0.3, 0.3) elseif secs and secs < 120 then btn.timer:SetTextColor(1, 0.82, 0) else btn.timer:SetTextColor(0.8, 1, 0.8) end end local function SetTimerFromSeconds(btn, timeLeft, untilCancelled, showTimer) if not showTimer then btn.timer:Hide() return end if untilCancelled == 1 or not timeLeft or timeLeft == 0 or timeLeft >= 99999 then btn.timer:SetText("N/A") btn.timer:SetTextColor(0.6, 0.6, 0.6) else btn.timer:SetText(FormatBuffTime(timeLeft)) if timeLeft < 30 then btn.timer:SetTextColor(1, 0.3, 0.3) elseif timeLeft < 120 then btn.timer:SetTextColor(1, 0.82, 0) else btn.timer:SetTextColor(0.8, 1, 0.8) end end btn.timer:Show() end function MB:UpdateBuffs() if not self.buffSlots then return end if self._simulating then return end local db = GetDB() local showTimer = db.showTimer ~= false local slotIdx = 0 for i = 0, 31 do local buffIndex, untilCancelled = GetPlayerBuff(i, "HELPFUL") if buffIndex and buffIndex >= 0 then slotIdx = slotIdx + 1 if slotIdx > MAX_BUFFS then break end local btn = self.buffSlots[slotIdx] local texture = GetPlayerBuffTexture(buffIndex) if texture then btn.icon:SetTexture(texture) btn.buffIndex = buffIndex btn._sfSimulated = false btn._isWeaponEnchant = false btn._weaponSlotID = nil local apps = GetPlayerBuffApplications(buffIndex) if apps and apps > 1 then btn.count:SetText(apps) btn.count:Show() else btn.count:SetText("") btn.count:Hide() end local timeLeft = GetPlayerBuffTimeLeft(buffIndex) SetTimerFromSeconds(btn, timeLeft, untilCancelled, showTimer) btn:SetBackdropBorderColor(0.25, 0.25, 0.30, 1) btn:Show() else btn:Hide() btn.buffIndex = -1 btn._isWeaponEnchant = false btn._weaponSlotID = nil end end end local hasMainHandEnchant, mainHandExpiration, mainHandCharges, hasOffHandEnchant, offHandExpiration, offHandCharges = GetWeaponEnchantInfo() if hasMainHandEnchant then slotIdx = slotIdx + 1 if slotIdx <= MAX_BUFFS then local btn = self.buffSlots[slotIdx] local texture = GetInventoryItemTexture("player", 16) if texture then btn.icon:SetTexture(texture) btn.buffIndex = -1 btn._sfSimulated = false btn._isWeaponEnchant = true btn._weaponSlotID = 16 if mainHandCharges and mainHandCharges > 0 then btn.count:SetText(mainHandCharges) btn.count:Show() else btn.count:SetText("") btn.count:Hide() end local timeLeft = mainHandExpiration and (mainHandExpiration / 1000) or 0 SetTimerFromSeconds(btn, timeLeft, 0, showTimer) btn:SetBackdropBorderColor(WEAPON_ENCHANT_COLOR.r, WEAPON_ENCHANT_COLOR.g, WEAPON_ENCHANT_COLOR.b, 1) btn:Show() end end end if hasOffHandEnchant then slotIdx = slotIdx + 1 if slotIdx <= MAX_BUFFS then local btn = self.buffSlots[slotIdx] local texture = GetInventoryItemTexture("player", 17) if texture then btn.icon:SetTexture(texture) btn.buffIndex = -1 btn._sfSimulated = false btn._isWeaponEnchant = true btn._weaponSlotID = 17 if offHandCharges and offHandCharges > 0 then btn.count:SetText(offHandCharges) btn.count:Show() else btn.count:SetText("") btn.count:Hide() end local timeLeft = offHandExpiration and (offHandExpiration / 1000) or 0 SetTimerFromSeconds(btn, timeLeft, 0, showTimer) btn:SetBackdropBorderColor(WEAPON_ENCHANT_COLOR.r, WEAPON_ENCHANT_COLOR.g, WEAPON_ENCHANT_COLOR.b, 1) btn:Show() end end end for j = slotIdx + 1, MAX_BUFFS do local btn = self.buffSlots[j] btn:Hide() btn.buffIndex = -1 btn._isWeaponEnchant = false btn._weaponSlotID = nil end self:UpdateDebuffs() self:AnchorDebuffs() end function MB:UpdateDebuffs() if not self.debuffSlots then return end if self._simulating then return end local db = GetDB() local showTimer = db.showTimer ~= false if db.showDebuffs == false then for i = 1, MAX_DEBUFFS do self.debuffSlots[i]:Hide() end if self.debuffContainer then self.debuffContainer:Hide() end return end if self.debuffContainer then self.debuffContainer:Show() end local slotIdx = 0 for i = 0, 15 do local buffIndex, untilCancelled = GetPlayerBuff(i, "HARMFUL") if buffIndex and buffIndex >= 0 then slotIdx = slotIdx + 1 if slotIdx > MAX_DEBUFFS then break end local btn = self.debuffSlots[slotIdx] local texture = GetPlayerBuffTexture(buffIndex) if texture then btn.icon:SetTexture(texture) btn.buffIndex = buffIndex btn._sfSimulated = false local apps = GetPlayerBuffApplications(buffIndex) if apps and apps > 1 then btn.count:SetText(apps) btn.count:Show() else btn.count:SetText("") btn.count:Hide() end local timeLeft = GetPlayerBuffTimeLeft(buffIndex) SetTimerFromSeconds(btn, timeLeft, untilCancelled, showTimer) local debuffType = nil SFrames.Tooltip:ClearLines() SFrames.Tooltip:SetPlayerBuff(buffIndex) local dTypeStr = SFramesScanTooltipTextRight1 and SFramesScanTooltipTextRight1:GetText() if dTypeStr and dTypeStr ~= "" then debuffType = dTypeStr end local c = DEBUFF_TYPE_COLORS[debuffType] or DEBUFF_DEFAULT_COLOR btn:SetBackdropBorderColor(c.r, c.g, c.b, 1) btn:Show() else btn:Hide() btn.buffIndex = -1 end end end for j = slotIdx + 1, MAX_DEBUFFS do self.debuffSlots[j]:Hide() self.debuffSlots[j].buffIndex = -1 end end -------------------------------------------------------------------------------- -- Simulation (2 rows each for buff & debuff) -------------------------------------------------------------------------------- local SIM_BUFFS = { -- Row 1 { tex = "Interface\\Icons\\Spell_Holy_WordFortitude", label = "Power Word: Fortitude", desc = "Stamina +54", time = "N/A" }, { tex = "Interface\\Icons\\Spell_Shadow_AntiShadow", label = "Shadow Protection", desc = "Shadow Resistance +60", time = "N/A" }, { tex = "Interface\\Icons\\Spell_Holy_MagicalSentry", label = "Arcane Intellect", desc = "Intellect +31", time = "42m" }, { tex = "Interface\\Icons\\Spell_Nature_Regeneration", label = "Mark of the Wild", desc = "All stats +12", time = "38m" }, { tex = "Interface\\Icons\\Spell_Holy_GreaterBlessingofKings", label = "Blessing of Kings", desc = "All stats +10%", time = "7m" }, { tex = "Interface\\Icons\\Spell_Holy_PrayerOfHealing02", label = "Renew", desc = "Heals 152 over 15 sec", time = "12s" }, { tex = "Interface\\Icons\\Spell_Holy_DivineSpirit", label = "Divine Spirit", desc = "Spirit +40", time = "N/A" }, { tex = "Interface\\Icons\\Spell_Fire_SealOfFire", label = "Fire Shield", desc = "Fire damage absorb", time = "25m" }, -- Row 2 { tex = "Interface\\Icons\\Spell_Holy_PowerWordShield", label = "Power Word: Shield", desc = "Absorbs 942 damage", time = "28s" }, { tex = "Interface\\Icons\\Spell_Nature_Lightning", label = "Lightning Shield", desc = "3 charges", time = "9m", count = 3 }, { tex = "Interface\\Icons\\Spell_Holy_SealOfWisdom", label = "Blessing of Wisdom", desc = "MP5 +33", time = "5m" }, { tex = "Interface\\Icons\\Spell_Nature_UndyingStrength", label = "Thorns", desc = "Nature damage on hit", time = "N/A" }, { tex = "Interface\\Icons\\Spell_Nature_Invisibilty", label = "Innervate", desc = "Spirit +400%", time = "18s" }, { tex = "Interface\\Icons\\Spell_Holy_PowerInfusion", label = "Power Infusion", desc = "+20% spell damage", time = "14s" }, { tex = "Interface\\Icons\\Spell_Holy_SealOfValor", label = "Blessing of Sanctuary", desc = "Block damage reduced", time = "3m" }, { tex = "Interface\\Icons\\Spell_Nature_EnchantArmor", label = "Nature Resistance", desc = "Nature Resistance +60", time = "1h12m" }, } local SIM_DEBUFFS = { -- Row 1 { tex = "Interface\\Icons\\Spell_Shadow_CurseOfTounable", label = "Curse of Tongues", desc = "Casting 50% slower", time = "28s", dtype = "Curse" }, { tex = "Interface\\Icons\\Spell_Shadow_UnholyStrength", label = "Weakened Soul", desc = "Cannot be shielded", time = "15s", dtype = "Magic" }, { tex = "Interface\\Icons\\Ability_Creature_Disease_02", label = "Corrupted Blood", desc = "Inflicts disease damage", time = "N/A", dtype = "Disease" }, { tex = "Interface\\Icons\\Spell_Nature_CorrosiveBreath", label = "Deadly Poison", desc = "Inflicts Nature damage", time = "8s", dtype = "Poison" }, { tex = "Interface\\Icons\\Spell_Shadow_Possession", label = "Fear", desc = "Feared for 8 sec", time = "6s", dtype = "Magic" }, { tex = "Interface\\Icons\\Spell_Shadow_ShadowWordPain", label = "Shadow Word: Pain", desc = "Shadow damage over time", time = "24s", dtype = "Magic" }, { tex = "Interface\\Icons\\Spell_Shadow_AbominationExplosion", label = "Mortal Strike", desc = "Healing reduced 50%", time = "5s", dtype = nil }, { tex = "Interface\\Icons\\Spell_Frost_FrostArmor02", label = "Frostbolt", desc = "Movement slowed 40%", time = "4s", dtype = "Magic" }, -- Row 2 { tex = "Interface\\Icons\\Spell_Shadow_CurseOfSargeras", label = "Curse of Agony", desc = "Shadow damage over time", time = "22s", dtype = "Curse" }, { tex = "Interface\\Icons\\Spell_Nature_Slow", label = "Crippling Poison", desc = "Movement slowed 70%", time = "10s", dtype = "Poison" }, { tex = "Interface\\Icons\\Spell_Shadow_CurseOfMannoroth", label = "Curse of Elements", desc = "Resistance reduced 75", time = "N/A", dtype = "Curse" }, { tex = "Interface\\Icons\\Ability_Creature_Disease_03", label = "Devouring Plague", desc = "Disease damage + heal", time = "20s", dtype = "Disease" }, } function MB:SimulateBuffs() if not self.buffSlots or not self.debuffSlots then return end self._simulating = true for i = 1, MAX_BUFFS do local btn = self.buffSlots[i] local sim = SIM_BUFFS[i] if sim then btn.icon:SetTexture(sim.tex) btn.buffIndex = -1 btn._sfSimulated = true btn._isWeaponEnchant = false btn._weaponSlotID = nil btn._sfSimLabel = sim.label btn._sfSimDesc = sim.desc btn.timer:SetText(sim.time) ApplyTimerColor(btn, sim.time) btn.timer:Show() if sim.count and sim.count > 1 then btn.count:SetText(sim.count) btn.count:Show() else btn.count:Hide() end btn:SetBackdropBorderColor(0.25, 0.25, 0.30, 1) btn:Show() else btn:Hide() end end for i = 1, MAX_DEBUFFS do local btn = self.debuffSlots[i] local sim = SIM_DEBUFFS[i] if sim then btn.icon:SetTexture(sim.tex) btn.buffIndex = -1 btn._sfSimulated = true btn._sfSimLabel = sim.label btn._sfSimDesc = sim.desc btn.timer:SetText(sim.time) ApplyTimerColor(btn, sim.time) btn.timer:Show() btn.count:Hide() local c = DEBUFF_TYPE_COLORS[sim.dtype] or DEBUFF_DEFAULT_COLOR btn:SetBackdropBorderColor(c.r, c.g, c.b, 1) btn:Show() else btn:Hide() end end if self.debuffContainer then self.debuffContainer:Show() end self:AnchorDebuffs() end function MB:StopSimulation() self._simulating = false self:UpdateBuffs() end function MB:Refresh() if not self.buffContainer then return end local db = GetDB() if db.enabled == false then self.buffContainer:Hide() if self.debuffContainer then self.debuffContainer:Hide() end return end self:ApplyPosition() self:ApplyLayout() if not self._simulating then self:UpdateBuffs() else self:AnchorDebuffs() end self.buffContainer:Show() end function MB:Initialize() local db = GetDB() if db.enabled == false then return end HideBlizzardBuffs() self.buffContainer = CreateFrame("Frame", "SFramesMBuffContainer", UIParent) self.buffContainer:SetWidth(400) self.buffContainer:SetHeight(200) self.buffContainer:SetFrameStrata("LOW") self.debuffContainer = CreateFrame("Frame", "SFramesMDebuffContainer", UIParent) self.debuffContainer:SetWidth(400) self.debuffContainer:SetHeight(100) self.debuffContainer:SetFrameStrata("LOW") self.buffSlots = {} for i = 1, MAX_BUFFS do self.buffSlots[i] = CreateSlot(self.buffContainer, "SFramesMBuff", i, true) end self.debuffSlots = {} for i = 1, MAX_DEBUFFS do self.debuffSlots[i] = CreateSlot(self.debuffContainer, "SFramesMDebuff", i, false) end self:ApplyPosition() self:ApplyLayout() self.updater = CreateFrame("Frame", nil, self.buffContainer) self.updater.timer = 0 self.updater:SetScript("OnUpdate", function() this.timer = this.timer + arg1 if this.timer >= UPDATE_INTERVAL then MB:UpdateBuffs() this.timer = 0 end end) self:UpdateBuffs() end