view KBF.lua @ 64:e5c07fdfb70b

remove the old hacked weapon enchant stuff since SAH properly supports it now, but turn it off because due to bugs either in KBF or SAH (both?), you get overlapping messed up displays whenever you have extra, non-buff frames
author Chris Mellon <arkanes@gmail.com>
date Fri, 02 Dec 2011 06:15:35 -0600
parents 31eac67dd283
children e19d0380b9f3
line wrap: on
line source
local _, kbf = ...

KBF = kbf -- make global for debugging

local kbf = LibStub("AceAddon-3.0"):NewAddon(kbf, "KBF", "AceEvent-3.0", "AceConsole-3.0")


function kbf:OnInitialize()
	-- config settings - account wide shared profile by default
	self.db = LibStub("AceDB-3.0"):New("KBFSavedVars", self.defaultConfig, true)
	-- create frames here so that they will be correctly stored in location cache by 
	-- the UI.
	self.anchor, self.secureHeader, self.consolidateHeader, self.consolidateProxy = self:CreateCoreFrames()
    self.debuffFrames = {}
    self:RegisterEvent("UNIT_AURA")
    self:RegisterEvent("UNIT_ENTERING_VEHICLE", "PollForVehicleChange")
    self:RegisterEvent("UNIT_EXITING_VEHICLE", "PollForVehicleChange")
    LibStub("AceConfig-3.0"):RegisterOptionsTable("KBF", self.options);
    self.profilesFrame = LibStub("AceConfigDialog-3.0"):AddToBlizOptions("KBF", "KBF");
    self:RegisterChatCommand("kbf", "ToggleAnchor")
    
    self.oocQueue = {}
end

function kbf:OnEnable()
	-- set up the countdown timer
    -- TODO: Fancy enable/disable based on whether you have any timed buffs.
    -- Not a big deal, how often do you care about that
    -- also TODO: Maybe should bucket OnUpdates somehow
    -- AceTimer repeating events can only happen at 0.1 seconds, which is probably
    -- fast enough for updating, but makes the animation look jerky
    -- need to experiment with using animation groups
    self.update = CreateFrame("FRAME")
    self.update:SetScript("OnUpdate", function() self:OnUpdate() end)
    self.dirty = true -- force an immediate scan on login
    self:HideBlizzardBuffFrames()
end
-- naming convention
-- a "frame" is the top-level button (secure button from the header, or one I make myself)
-- that will contain the UI information about the buff
-- a "bar" is a frame that has the icon, status bar, ect associated with it

-- Secure aura header doesn't self-bind to vehicle,
-- so this only works out of combat. But thats better than nothing...
function kbf:PollForVehicleChange(event, unit)
    if unit ~= "player" then return end
    self.dirty = true
    local function performSwap()
    	if UnitHasVehicleUI("player") then
			-- only swap if we're in a "real" vehicle with its own actions
        	-- There is possibly a timing issue here where
        	-- we have set the poll flag but the unit is not 
        	-- actually "in" the vehicle yet. I'm hoping thats 
        	-- handled by using exited/entered events instead of exiting/entering
            self.secureHeader:SetAttribute("unit", "vehicle")
        else
            self.secureHeader:SetAttribute("unit", "player")
        end
    end
    self:QueueForOOC(performSwap)
end

function kbf:HideBlizzardBuffFrames()
    local function HideBlizFrame(frame)
        if not frame then return end
        frame:UnregisterAllEvents()
        frame:SetScript("OnUpdate", nil)
        frame:Hide()
        frame.Show = function() end
    end
    HideBlizFrame(BuffFrame)
    HideBlizFrame(ConsolidatedBuffs)
    HideBlizFrame(TemporaryEnchantFrame)
    
end

-- enqueues a callable that will be run once in-combat lockdown is past
-- all callables will be executed in a single run, in the order they were enqueued
-- if called when OOC, the function will be called immediately, unless the alwaysQueue parameter is true, 
-- in which case it will be appended normally
function kbf:QueueForOOC(func, alwaysQueue)
	if InCombatLockdown() or alwaysQueue then
		tinsert(self.oocQueue, func)
	else
		func()
	end
end

function kbf:OnUpdate()   
    -- TODO: only start this polling when we leave combat?
    while #self.oocQueue > 0  and not InCombatLockdown() do
		local func = table.remove(self.oocQueue)
		func()
    end
    
    local unit = self.secureHeader:GetAttribute("unit")
    local buffCount = 0
    for idx=1,99 do
        local frame = self.secureHeader:GetAttribute("child"..idx)
        if not (frame and frame:IsShown()) then break end
        local hasbuff = UnitAura(unit, frame:GetAttribute("index"))
        buffCount = buffCount + 1
        if self.dirty then 
            if self:BindBarToBuff(frame, unit) then break end
        end
        self:UpdateBarExpirationTime(frame)
        -- Don't forget to refresh shown tooltips
        if (GameTooltip:IsOwned(frame)) then
            self:OnEnter(frame)
        end
    end
    -- consolidated buffs
    if self.consolidateProxy:IsShown() then
		for idx=1,99 do
			local frame = self.consolidateHeader:GetAttribute("child"..idx)
			if not (frame and frame:IsShown()) then break end
			if self.dirty then 
				if self:BindBarToBuff(frame, unit) then break end
			end
			self:UpdateBarExpirationTime(frame)
			-- Don't forget to refresh shown tooltips
			if ( GameTooltip:IsOwned(frame) ) then
				self:OnEnter(frame)
			end
		end
		buffCount = buffCount+1
	end
	-- SAH correctly binds the weapon enchant templates now, but when temp enchants
	-- are present and used, it seems that it doesn't correctly hide un-bound 
	-- buff frames, which breaks all the layout and so forth.
	for weapon=3,1,-1 do
		local tempEnchant = self.secureHeader:GetAttribute("tempEnchant"..weapon)
		if tempEnchant and tempEnchant:IsShown() then
			self:BindBarToWeaponEnchant(tempEnchant)
			self:UpdateBarExpirationTime(tempEnchant)
			buffCount = buffCount + 1
		end
	end
	
	-- debuffs
	-- Since debuffs aren't cancellable, don't need to use the secure header
	-- for them. This could be rewritten to support useful features like
	-- sorting & scaling and stuff. Honestly, should at least be alphabetical.
    for idx=1,99 do
        local frame = self.debuffFrames[idx]
        if self.dirty then
            local name, rank, icon, stacks, debuffType, duration, expirationTime = UnitAura(unit, idx, "HARMFUL")
            if not name then 
                -- out of debuffs, hide all the rest of them
                for jdx = idx, 99 do
                    local bar = self.debuffFrames[jdx]
                    if bar then bar:Hide() else break end
                end
                break 
            end
            if not frame then
                frame = self:ConstructBar(nil, 1, 0, 0)
                self.debuffFrames[idx] = frame
            end
            self:SetBarAppearance(frame, name, icon, stacks, duration, expirationTime)
            frame:ClearAllPoints()
            -- position it under all the buffs, with a half-bar spacing
            frame:SetPoint("TOP", self.secureHeader, "TOP", 0, (buffCount * -16) - 8)
            frame:Show()
            frame.filter = "HARMFUL"
            frame.unit = unit
            frame.index = idx
            frame:SetScript("OnEnter", function() kbf:OnEnter(frame) end)
            frame:SetScript("OnLeave", function() GameTooltip:Hide() end)
            frame:EnableMouse(true)
            buffCount = buffCount + 1
        else
            -- not dirty, so no frame means we're done
            if not frame then break end
        end
        self:UpdateBarExpirationTime(frame)
        if ( GameTooltip:IsOwned(frame) ) then
            self:OnEnter(frame)
        end
    end
    self.dirty = nil
end

function kbf:UNIT_AURA(event, unit)
    if unit ~= self.secureHeader:GetAttribute("unit") then return end
    self.dirty = true
end

function kbf:UpdateBarExpirationTime(frame)
    if frame.expirationTime then
        local remaining = frame.expirationTime - GetTime()
        remaining = math.max(0, remaining)
        local perc = remaining / frame.duration
        frame.timertext:SetText(self:FormatTimeText(remaining))
        frame.statusbar:SetValue(remaining)
    end
end

local gratt = LibStub:GetLibrary("LibGratuity-3.0")

function kbf:BindBarToWeaponEnchant(parentFrame, slotOverride)
    -- allow passing of explicit slot in order to work around aura header bug
    local slot = slotOverride or parentFrame:GetAttribute("target-slot")
    local itemIndex = slot - 15 -- 1MH, 2OF
    local RETURNS_PER_ITEM = 3
    local hasEnchant, remaining, enchantCharges = select(RETURNS_PER_ITEM * (itemIndex - 1) + 1, GetWeaponEnchantInfo())
    -- remaining time is in milliseconds
    if not hasEnchant then return end -- this should never happen
    local remaining = remaining / 1000
    
    local icon = GetInventoryItemTexture("player", slot)
    local maxDuration
    if select(2, UnitClass("player")) == "ROGUE" then
    	-- Rogues are probably using poisons, which are an hour
    	maxDuration = 60 * 60
    else
    	-- everyone else is probably using something thats a half hour
    	maxDuration = 30 * 60
    end
    local duration = max(maxDuration, remaining)
    local expirationTime = GetTime() + remaining
    local name = GetItemInfo(GetInventoryItemID("player", slot))
    -- try to figure out what the weapon enchant is
    -- tooltip string -> {spellid, duration}
    local knownEnchants = {
    	["Flametongue"] = {8024, 30*60},
    	["Frostbrand"] = {8033, 30*60},
    	["Earthliving"] = {51730, 30*60},
		["Windfury"] = {8232, 30*60},
    	["Instant Poison"] = {8680, 60*60},
    	["Wound Poison"] = {13218, 60*60},
    	["Deadly Poison"] = {2823, 60*60},
    
    }
    local spellId = nil
	if gratt then
		gratt:SetInventoryItem("player", slot)
		for tag, info in pairs(knownEnchants) do
			if gratt:Find(tag) then 
				spellId, duration = unpack(info)
				name, _, _ = GetSpellInfo(spellId)
				local slots = {[16] = "Main Hand", [17] = "Off Hand", [18] = "Thrown"}
				name = tag .. " (" .. slots[slot] .. ")"
				break
			end
		end
	end
    parentFrame.spellId = spellId
    if not parentFrame.icon then
        self:ConstructBar(parentFrame, 1, 0, 1)
    end
    self:SetBarAppearance(parentFrame, name, icon, enchantCharges, duration, expirationTime)
end

function kbf:BindBarToBuff(parentFrame, unit)
    local index = parentFrame:GetAttribute("index")
    local filter = parentFrame:GetAttribute("filter")
    local name, rank, icon, stacks, debuffType, duration, expirationTime, 
        unitCaster, isStealable, shouldConsolidate, spellId = UnitAura(unit, index, filter)
    if not name then return end
    if not parentFrame.icon then
        self:ConstructBar(parentFrame)
    end
    self:SetBarAppearance(parentFrame, name, icon, stacks, duration, expirationTime)
end

function kbf:SetBarAppearance(parentFrame, name, icon, stacks, duration, expirationTime)
    parentFrame.icon:SetNormalTexture(icon)
    if stacks and stacks > 0 then
        parentFrame.text:SetText(string.format("%s(%d)", name, stacks))
    else
        parentFrame.text:SetText(name)
    end
    parentFrame.timertext:SetText(self:FormatTimeText(duration))
    -- store duration information
    if duration and duration > 0 then
        parentFrame.expirationTime = expirationTime
        parentFrame.duration = duration
        parentFrame.statusbar:SetMinMaxValues(0, duration)
    else
        parentFrame.expirationTime = nil
        parentFrame.duration = 0
        parentFrame.statusbar:SetMinMaxValues(0,1)
        parentFrame.statusbar:SetValue(1)
    end
end

-- expects time seconds
function kbf:FormatTimeText(time)
    if not time or time == 0 then return "" end
    local timetext
    local h = floor(time/3600)
    local m = time - (h*3600)
    m = floor(m/60)
    local s = time - ((h*3600) + (m*60))
    if h > 0 then
        timetext = ("%d:%02d"):format(h, m)
    elseif m > 0 then
        timetext = string.format("%d:%02d", m, floor(s))
    elseif s < 10 then
        timetext = string.format("%1.1f", s)
    else
        timetext = string.format("%.0f", floor(s))
    end
    return timetext
end

function KBF:OnEnter(button, motion)
    -- this is for the secure buttons, so use the attributes
    local unit = SecureButton_GetModifiedUnit(button) or button.unit -- will perform vehicle toggle
    local filter = button:GetAttribute("filter") or button.filter 
    local index = button:GetAttribute("index") or button.index
    if unit and filter and index then
        -- I'd like a better place to position this but it's funky for right now, handle it later
        GameTooltip:SetOwner(button, "ANCHOR_BOTTOMLEFT");
        GameTooltip:SetFrameLevel(button:GetFrameLevel() + 2);
        GameTooltip:SetUnitAura(unit, index, filter);
        return
    end
    local slot = button:GetAttribute("target-slot") -- temp enchant
    if slot then
		GameTooltip:SetOwner(button, "ANCHOR_BOTTOMLEFT");
		GameTooltip:SetFrameLevel(button:GetFrameLevel() + 2);
    	if button.spellId then
    		-- TODO: This might be too big of a tooltip to care about that much.
    		-- Maybe I should just have a single line with the weapon name
    		--GameTooltip:SetInventoryItem(unit, slot)
    		local name = GetItemInfo(GetInventoryItemID("player", slot))
    		local r, g, b = GetItemQualityColor(GetInventoryItemQuality("player", slot))
    		GameTooltip:SetText(name, r, g, b)
			local slots = {[16] = "Main Hand", [17] = "Off Hand", [18] = "Thrown"}
			GameTooltip:AddLine(slots[slot])
    		GameTooltip:AddLine(" ")
    		GameTooltip:AddSpellByID(button.spellId)
    	else
    		GameTooltip:SetInventoryItem(unit, slot)
    	end
	end
end

-- creates a icon + statusbar bar
function kbf:ConstructBar(frame, r, g, b)
    local texture = "Interface\\TargetingFrame\\UI-StatusBar"
    -- Because of secureframe suckiness, these height & width numbers
    -- have to be consistent with the stuff in KBF.xml
    local height = self.staticConfig.BAR_HEIGHT
    local width = self.staticConfig.BAR_WIDTH -- this is the width *without* the icon
    local font, _, style = GameFontHighlight:GetFont()
    local r = r or 0
    local g = g or 1
    local b = b or 0
    local bgcolor = {r, g, b, 0.5}
    local color = {r, g, b, 1}
    local fontsize = 11
    local timertextwidth = fontsize * 3.6
    local textcolor = {1, 1, 1, 1}
    local timertextcolor = {1, 1, 1, 1}
    if not frame then
        frame = CreateFrame("Button", nil, UIParent) -- the "top level" frame that represents the bar as a whole
        frame:SetHeight(height)
        frame:SetWidth(width + height)
    end
    local bar = frame
    bar.icon = CreateFrame("Button", nil, bar) -- the icon
    bar.statusbarbg = CreateFrame("StatusBar", nil, bar) -- the bars background
    bar.statusbar = CreateFrame("StatusBar", nil, bar) -- and the bars foreground
    bar.text = bar.statusbar:CreateFontString(nil, "OVERLAY") -- the label text
    bar.timertext = bar.statusbar:CreateFontString(nil, "OVERLAY") -- and the timer text

    -- the icon
    bar.icon:ClearAllPoints()
    bar.icon:SetPoint("LEFT", bar, "LEFT", 0, 0)
    -- icons are square
    bar.icon:SetWidth(height)
    bar.icon:SetHeight(height)
    --bar.icon:EnableMouse(false)
    -- the status bar background & foreground
    local function setupStatusBar(sb, color)
        sb:ClearAllPoints()
        sb:SetHeight(height)
        sb:SetWidth(width)
        -- offset the height of the frame on the x-axis for the icon.
        sb:SetPoint("TOPLEFT", bar, "TOPLEFT", height, 0)
        sb:SetStatusBarTexture(texture)
        sb:GetStatusBarTexture():SetVertTile(false)
        sb:GetStatusBarTexture():SetHorizTile(false)
        sb:SetStatusBarColor(unpack(color))
        sb:SetMinMaxValues(0,1)
        sb:SetValue(1)
    end
    setupStatusBar(bar.statusbarbg, bgcolor)
    setupStatusBar(bar.statusbar, color)
    bar.statusbarbg:SetFrameLevel(bar.statusbarbg:GetFrameLevel()-1) -- make sure the bg frame stays in the back
    -- timer text
    bar.timertext:SetFontObject(GameFontHighlight)
    bar.timertext:SetFont(GameFontHighlight:GetFont())
    bar.timertext:SetHeight(height)
    bar.timertext:SetWidth(timertextwidth)
    bar.timertext:SetPoint("LEFT", bar.statusbar, "LEFT", 2, 0)
    bar.timertext:SetJustifyH("LEFT")
    bar.timertext:SetText("time")
    bar.timertext:SetTextColor(timertextcolor[1], timertextcolor[2], timertextcolor[3], timertextcolor[4])

    -- and the label text
    bar.text:SetFontObject(GameFontHighlight)
    bar.text:SetFont(GameFontHighlight:GetFont())
    bar.text:SetHeight(height)
    bar.text:SetPoint("LEFT", bar.timertext, "RIGHT", 0, 0)
    bar.text:SetPoint("RIGHT", bar.statusbar, "RIGHT", 0, 0)
    bar.text:SetJustifyH("LEFT")
    bar.text:SetText("text")
    bar.text:SetTextColor(textcolor[1], textcolor[2], textcolor[3], textcolor[4])
    return bar
end

function kbf:CreateCoreFrames()
	-- this is the visible anchor frame that the user interacts with 
	-- to move the buffs around
	local height = self.staticConfig.BAR_HEIGHT
    local width = self.staticConfig.BAR_WIDTH -- this is the width *without* the icon
    local anchor = CreateFrame("FRAME", "KBFAnchorFrame", UIParent)
    anchor:SetClampedToScreen(true)
    anchor:SetBackdrop({bgFile = "Interface/Tooltips/UI-Tooltip-Background", 
                         edgeFile = "Interface/Tooltips/UI-Tooltip-Border", 
                         tile = true, tileSize = height, edgeSize = 12,
                         insets = { left = 4, right = 4, top = 4, bottom = 4 },
                         })
    local text = anchor:CreateFontString(nil, "OVERLAY") -- the label text
    text:SetFontObject(GameFontHighlight)
    text:SetFont(GameFontHighlight:GetFont())
    text:SetPoint("TOPLEFT", anchor, "TOPLEFT", 0, 0)
    text:SetPoint("BOTTOMRIGHT", anchor, "BOTTOMRIGHT", 0, 0)
    text:SetText("KBF ANCHOR")
    anchor:SetWidth(height + width)
    anchor:SetHeight(height)
    -- movability
    anchor:EnableMouse(true)
    anchor:SetMovable(true)
    anchor:RegisterForDrag("LeftButton")
    anchor:SetScript("OnDragStart", anchor.StartMoving)
    anchor:SetScript("OnDragStop",  anchor.StopMovingOrSizing)
    anchor:ClearAllPoints()
    anchor:SetPoint("TOPRIGHT", UIParent, "TOPRIGHT", 0, 0)
    anchor:Hide()
    -- this is the parent & host for the secure aura buttons. 
    
	local secureHeader = CreateFrame("FRAME", "KBFBuffFrame", UIParent, "SecureAuraHeaderTemplate")
	self:SetCommonSecureHeaderAttributes(secureHeader)
	if self.db.profile.consolidateBuffs then
	    secureHeader:SetAttribute("consolidateTo", 99)
	end
    secureHeader:SetPoint("TOP", anchor, "TOP", 0, 0)
    
    -- this is the "button" in the aura flow that represents the consolidated buffs.
    -- pre-creating it here in order to perform customization
    local consolidateProxy = CreateFrame("BUTTON", nil, UIParent, "SecureHandlerClickTemplate")
    consolidateProxy:SetNormalTexture("Interface\\TargetingFrame\\UI-StatusBar")
    consolidateProxy:SetWidth(200 +16)
	consolidateProxy:SetHeight(16)
    secureHeader:SetAttribute("consolidateProxy", consolidateProxy)
    --secureHeader:SetFrameRef("proxy", consolidateProxy)

    
    -- this is the equivilent of the secureHeader for the consolidated buffs
    -- pre-creating again, so we can customize/size/position it
    local consolidateHeader = CreateFrame("FRAME", "KBFConsolidatedAnchorFrame", consolidateProxy)
    self:SetCommonSecureHeaderAttributes(consolidateHeader)
    secureHeader:SetAttribute("consolidateHeader", consolidateHeader)
	consolidateProxy:SetAttribute("header", consolidateHeader);
	consolidateProxy:SetFrameRef("header", consolidateHeader)
	
	consolidateProxy:SetAttribute("_onclick", [[
    	local frame = self:GetFrameRef("header")
    	if frame:IsShown() then frame:Hide() else frame:Show() end
    ]])
    consolidateProxy:EnableMouse(true)
    consolidateProxy:RegisterForClicks("AnyUp")
	
	-- position it relative to the proxy, so it can appear where we want it
    consolidateHeader:SetPoint("TOPRIGHT", anchor, "TOPLEFT", 0, 0)
	consolidateHeader:SetWidth(height + width)
	consolidateHeader:SetHeight(height)
	consolidateHeader:Show()
	
    return anchor, secureHeader, consolidateHeader, consolidateProxy
end

--- sets the attributes needed by all the headers
function kbf:SetCommonSecureHeaderAttributes(frame)
    frame:SetAttribute("filter", "HELPFUL")
    frame:SetAttribute("toggleForVehicle", true) -- this doesn't actually work right now, but maybe it eventually will
    frame:SetAttribute("template", "KBFSecureUnitAuraTemplate")
    frame:SetAttribute("point", "TOP")
    frame:SetAttribute("wrapAfter", 100) -- required due to bugs in secure header
    frame:SetAttribute("xOffset", 0)
    frame:SetAttribute("yOffset", -16)
    frame:SetAttribute("minWidth",  216)
    frame:SetAttribute("minHeight", 16)
    frame:SetAttribute("unit", "player")
    frame:SetAttribute("sortMethod", "NAME")
    frame:SetAttribute("sortOrder", "-")
    
    frame:SetAttribute("weaponTemplate", "KBFSecureUnitAuraTemplate")
    -- TODO: Enabling temp enchant support breaks layout for regular buffs
    frame:SetAttribute("includeWeapons", nil)
    frame:Show() -- has to be shown, otherwise the child frames don't show
    return frame
end

function kbf:ShowAnchor()
    self.secureHeader:ClearAllPoints()
    self.secureHeader:SetPoint("TOP", self.anchor, "BOTTOM", 0, 0)
    self.anchor:Show()
end

function kbf:HideAnchor()
    self.secureHeader:ClearAllPoints()
    self.secureHeader:SetPoint("TOP", self.anchor, "TOP", 0, 0)
    self.anchor:Hide()
end

function kbf:ToggleAnchor()
    if self.anchor:IsShown() then
        self:HideAnchor()
    else
        self:ShowAnchor()
    end
end