Files
Nanami-UI/MinimapBuffs.lua
2026-03-16 13:48:46 +08:00

639 lines
23 KiB
Lua

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