Quantcast

Added Cooldown tracking

Kith [07-29-16 - 12:31]
Added Cooldown tracking
Expanded Notifications to include Cooldown announcements and alerts
Changed colour of 'helper' tooltips
Changed formatting of announce messages
Moved alert output options to their own subgroup
Added sample cooldown timers to Examples
Filename
Ellipsis/Cooldown.lua
Ellipsis/Core.lua
Ellipsis/Defaults.lua
Ellipsis/Ellipsis.toc
Ellipsis/Locales/Local_enUS.lua
Ellipsis/Notify.lua
Ellipsis/Overlays.lua
Ellipsis_Options/CooldownOptions.lua
Ellipsis_Options/Ellipsis_Options.toc
Ellipsis_Options/Locales/Local_enUS.lua
Ellipsis_Options/NotificationOptions.lua
Ellipsis_Options/Options.lua
diff --git a/Ellipsis/Cooldown.lua b/Ellipsis/Cooldown.lua
new file mode 100644
index 0000000..3f8f4b8
--- /dev/null
+++ b/Ellipsis/Cooldown.lua
@@ -0,0 +1,693 @@
+local Ellipsis		= _G['Ellipsis']
+local L				= LibStub('AceLocale-3.0'):GetLocale('Ellipsis')
+local LSM			= LibStub('LibSharedMedia-3.0')
+local Cooldown		= CreateFrame('Frame', nil, UIParent)
+local CooldownTimer	= {}
+
+local timerPool			= {}
+local activeTimers		= {}
+local activeTimersCount = 0
+
+local tagDataBase		= {0, 10, 60, 300, 900, 1800, 3600}
+local tagDataDetail		= {0, 2, 10, 30, 60, 120, 300, 600, 900, 1200, 1500, 1800, 2700, 3600}
+
+local math_pow, math_min = math.pow, math.min
+local tinsert, tremove = table.insert, table.remove
+local unpack, ipairs, pairs = unpack, ipairs, pairs
+
+local GetInventoryItemCooldown, GetInventoryItemID, GetItemInfo = GetInventoryItemCooldown, GetInventoryItemID, GetItemInfo
+local GetContainerItemCooldown, GetContainerItemID, GetContainerNumSlots = GetContainerItemCooldown, GetContainerItemID, GetContainerNumSlots
+local GetSpellBookItemInfo, GetSpellCooldown, GetSpellInfo = GetSpellBookItemInfo, GetSpellCooldown, GetSpellInfo
+local BOOKTYPE_PET, BOOKTYPE_SPELL = BOOKTYPE_PET, BOOKTYPE_SPELL
+
+local GetTime = GetTime
+
+local anchorData, cooldownDB
+
+-- variables configured by user options
+local blacklistITEM, blacklistSPELL, durationMin, durationMax
+local horizontal, length, thickness
+local onlyWhenTracking, sendAlerts
+local tickRate = 1 -- set to initial value to delay OnUpdate until options are configured
+
+-- pre calculated variables (based on user config)
+local maxTime, endPadding, workLength
+
+Ellipsis.Cooldown = Cooldown
+Ellipsis.CooldownTimer = CooldownTimer
+Ellipsis.Cooldown.activeTimers = activeTimers
+
+
+local function EventRegistration(event, register)
+	if (register) then
+		Cooldown:RegisterEvent(event)
+		Cooldown[event](Cooldown)
+	else
+		Cooldown:UnregisterEvent(event)
+	end
+end
+
+
+-- ------------------------
+-- COOLDOWN INIT
+-- ------------------------
+function Ellipsis:InitializeCooldowns()
+	-- unlike for all the aura handling, cooldowns are entirely self-contained (and only fully init'd if enabled)
+	anchorData		= self.db.profile.anchorData.CD
+	cooldownDB		= self.db.profile.cooldowns
+	blacklistITEM	= cooldownDB.blacklist.ITEM
+	blacklistSPELL	= cooldownDB.blacklist.SPELL
+
+	Cooldown:SetMovable(true)
+	Cooldown:SetClampedToScreen(true)
+	Cooldown.anchorID = 'CD' -- used to allow us to easily attach an overlay frame
+
+	Cooldown:Configure()
+end
+
+
+-- ------------------------
+-- ONUPDATE SCRIPT HANDLER
+-- ------------------------
+local throttle = 0
+
+local function OnUpdate(self, elapsed)
+	throttle = throttle + elapsed
+
+	if (throttle >= tickRate) then
+		local currentTime = GetTime()
+		local remaining, pos
+
+		for _, timer in pairs(activeTimers) do
+			remaining = timer.expireTime - currentTime
+
+			if (timer.expired) then -- cooldown complete, either pulse or release
+				if (remaining <= -0.6) then -- pulse is over, release
+					timer:Release()
+				else
+					timer:SetWidth(thickness * (1 + (4.2 * remaining * -1)))
+					timer:SetHeight(thickness * (1 + (4.2 * remaining * -1)))
+					timer:SetAlpha(1 + (2.1 * remaining))
+				end
+			else
+				pos = math_pow(remaining / maxTime, 0.4) * workLength
+				if (pos < 0) then pos = 0 elseif (pos > workLength) then pos = workLength end
+
+				if (horizontal) then
+					timer:SetPoint('CENTER', self.bar, 'LEFT', endPadding + pos, timer.offset)
+				else
+					timer:SetPoint('CENTER', self.bar, 'BOTTOM', timer.offset, endPadding + pos)
+				end
+
+				if (remaining <= 0) then
+					timer:SetExpired(currentTime)
+				end
+			end
+		end
+
+		throttle = throttle - tickRate
+	end
+end
+
+
+-- ------------------------
+-- EVENT HANDLERS
+-- ------------------------
+local bagUpdateLimiter = 0 -- this event gets called repeatedly, block those that happen in the same timestamp
+
+function Cooldown:BAG_UPDATE_COOLDOWN()
+	local currentTime = GetTime()
+
+	if (currentTime == bagUpdateLimiter) then return end
+
+	local start, duration
+	local name, icon, itemID
+	local timer
+
+	local index			= 1
+
+	for slot = 1, 19 do
+		start, duration = GetInventoryItemCooldown('player', slot)
+
+		if (duration > durationMin and duration < durationMax) then
+			itemID = GetInventoryItemID('player', slot)
+
+			if (not blacklistITEM[itemID]) then
+				name, _, _, _, _, _, _, _, _, icon = GetItemInfo(itemID)
+
+				timer = activeTimers['ITEM' .. itemID]
+
+				if (timer) then
+					if (start ~= timer.startTime) then
+						timer:Update(start, duration)
+					end
+
+					timer.updated = currentTime
+				else
+					CooldownTimer:New(currentTime, 'ITEM', itemID, name, icon, start, duration)
+				end
+			end
+		end
+	end
+
+	for bag = 0, 4 do
+		for slot = 1, GetContainerNumSlots(bag) do
+			start, duration = GetContainerItemCooldown(bag, slot)
+
+			if (duration > durationMin and duration < durationMax) then
+				itemID = GetContainerItemID(bag, slot)
+
+				if (not blacklistITEM[itemID]) then
+					name, _, _, _, _, _, _, _, _, icon = GetItemInfo(itemID)
+
+					timer = activeTimers['ITEM' .. itemID]
+
+					if (timer) then
+						if (start ~= timer.startTime) then
+							timer:Update(start, duration)
+						end
+
+						timer.updated = currentTime
+					else
+						CooldownTimer:New(currentTime, 'ITEM', itemID, name, icon, start, duration)
+					end
+				end
+			end
+		end
+	end
+
+	bagUpdateLimiter = currentTime
+end
+
+function Cooldown:PLAYER_EQUIPMENT_CHANGED()
+	self:BAG_UPDATE_COOLDOWN()
+end
+
+function Cooldown:PET_BAR_UPDATE_COOLDOWN()
+	local start, duration
+	local name, icon, spellID
+	local timer
+
+	local currentTime	= GetTime()
+	local index			= 1
+
+	start, duration = GetSpellCooldown(1, BOOKTYPE_PET)
+
+	while(start) do
+		if (duration > durationMin and duration < durationMax)then
+			_, spellID = GetSpellBookItemInfo(index, BOOKTYPE_PET)
+
+			if (not blacklistSPELL[spellID]) then
+				name , _, icon = GetSpellInfo(spellID)
+
+				timer = activeTimers['PET' .. spellID]
+
+				if (timer) then
+					if (start ~= timer.startTime) then -- check if cooldown has been altered (also fires when mid expiration)
+						timer:Update(start, duration)
+					end
+
+					timer.updated = currentTime
+				else -- make timer
+					CooldownTimer:New(currentTime, 'PET', spellID, name, icon, start, duration)
+				end
+			end
+		end
+
+		index = index + 1
+		start, duration = GetSpellCooldown(index, BOOKTYPE_PET)
+
+	end
+end
+
+function Cooldown:SPELL_UPDATE_COOLDOWN()
+	local start, duration
+	local name, icon, spellID
+	local timer
+
+	local currentTime	= GetTime()
+	local index			= 1
+
+	start, duration = GetSpellCooldown(1, BOOKTYPE_SPELL)
+
+	while(start) do
+		if (duration > durationMin and duration < durationMax) then
+			_, spellID = GetSpellBookItemInfo(index, BOOKTYPE_SPELL)
+
+			if (not blacklistSPELL[spellID]) then
+				name , _, icon = GetSpellInfo(spellID)
+
+				timer = activeTimers['SPELL' .. spellID]
+
+				if (timer) then
+					if (start ~= timer.startTime) then -- check if cooldown has been altered (also fires when mid expiration)
+						timer:Update(start, duration)
+					end
+
+					timer.updated = currentTime
+				else -- make timer
+					CooldownTimer:New(currentTime, 'SPELL', spellID, name, icon, start, duration)
+				end
+			end
+		end
+
+		index = index + 1
+		start, duration = GetSpellCooldown(index, BOOKTYPE_SPELL)
+
+	end
+
+	for _, timer in pairs(activeTimers) do
+		if (not timer.expired and timer.group == 'SPELL' and timer.updated < currentTime) then
+			timer:SetExpired()
+		end
+	end
+end
+
+
+-- ------------------------
+-- COOLDOWN CONFIGURATION
+-- ------------------------
+function Cooldown:Configure()
+	horizontal	= cooldownDB.horizontal
+	length		= cooldownDB.length
+	thickness	= cooldownDB.thickness
+
+	-- base config, done regardless of whether cooldowns are enabled (fairly lightweight and sets up anchor for overlay attachment)
+	self:SetHeight((horizontal and thickness or length) + 4)
+	self:SetWidth((horizontal and length or thickness) + 4)
+	self:SetAlpha(anchorData.alpha)
+	self:SetScale(anchorData.scale)
+
+	self:ClearAllPoints()
+	self:SetPoint(anchorData.point, UIParent, anchorData.point, anchorData.x / anchorData.scale, anchorData.y / anchorData.scale)
+
+	if (not cooldownDB.enabled) then
+		self:UnregisterEvent('BAG_UPDATE_COOLDOWN')
+		self:UnregisterEvent('PLAYER_EQUIPMENT_CHANGED')
+		self:UnregisterEvent('PET_BAR_UPDATE_COOLDOWN')
+		self:UnregisterEvent('SPELL_UPDATE_COOLDOWN')
+
+		for _, timer in pairs(activeTimers) do
+			timer:Release() -- clean out any active cooldown timers
+		end
+
+		Cooldown:Hide() -- hide display (and stop OnUpdate from firing)
+
+		return -- drop out quickly
+	end
+
+	-- cooldowns enabled, setup (and first time init if needed)
+	tickRate			= Ellipsis.db.profile.advanced.tickRate
+
+	onlyWhenTracking	= cooldownDB.onlyWhenTracking
+	sendAlerts			= (Ellipsis.db.profile.notify.coolPrematureAlerts or Ellipsis.db.profile.notify.coolCompleteAlerts)
+
+	maxTime				= cooldownDB.timeDisplayMax
+	endPadding			= thickness / 2
+	workLength			= length - thickness
+
+	if (not self.bar) then -- first time init
+		self:SetBackdrop({
+			bgFile		= 'Interface/Tooltips/UI-Tooltip-Background',
+			edgeFile	= 'Interface/Tooltips/UI-Tooltip-Border',
+			tile		= true,
+			tileSize	= 16,
+			edgeSize	= 6,
+			insets		= {left = 1, right = 1, top = 1, bottom = 1}
+		})
+
+		self.bar = self:CreateTexture(nil, 'BORDER')
+		self.bar:SetPoint('TOPLEFT', 2, -2)
+		self.bar:SetPoint('BOTTOMRIGHT', -2, 2)
+
+		self.tagFrame = CreateFrame('Frame', nil, self) -- all timeTags are attached to this frame to keep frame levels easier to handle
+		self.tagFrame:SetFrameLevel(self.tagFrame:GetFrameLevel() + 1)
+		self.tagFrame:SetAllPoints(self)
+
+		self.tags	= {}	-- holds 'time tag' widgets
+
+		-- setup script and event handlers
+		self:SetScript('OnUpdate', OnUpdate)
+		self:SetScript('OnEvent', function(self, event, ...)
+			self[event](self, ...)
+		end)
+	end
+
+	if (horizontal) then
+		self.bar:SetTexCoord(0, 1, 0, 1)
+	else
+		self.bar:SetTexCoord(1, 0, 0, 0, 1, 1, 0, 1)
+	end
+
+	self.bar:SetTexture(LSM:Fetch('statusbar', cooldownDB.texture))
+	self.bar:SetVertexColor(unpack(cooldownDB.colourBar))
+
+	self:SetBackdropColor(unpack(cooldownDB.colourBackdrop))
+	self:SetBackdropBorderColor(unpack(cooldownDB.colourBorder))
+
+	-- more variables needed for setting up time tags
+	local tagData	= cooldownDB.timeDetailed and tagDataDetail or tagDataBase
+	local tags		= self.tags
+	local tag, pos
+
+	for x = 1, #tags do tags[x]:Hide() end -- hide all existing timeTags
+
+	for i, time in ipairs(tagData) do
+		if (time <= maxTime) then
+			tag = tags[i]
+
+			if (not tag) then -- new tag needed
+				tag = self.tagFrame:CreateFontString(nil, 'ARTWORK', 'GameFontNormalSmall')
+				tags[i] = tag
+			end
+
+			tag:SetFont(LSM:Fetch('font', cooldownDB.timeFont), cooldownDB.timeFontSize)
+			tag:SetTextColor(unpack(cooldownDB.colourText))
+
+			tag:ClearAllPoints()
+
+			pos = (math_pow(time / maxTime, 0.4) * workLength)
+
+			if (pos == workLength) then -- special case if a tag entry is buffered at the 'end' of the bar
+				tag:SetPoint(horizontal and 'RIGHT' or 'TOP', self.bar, horizontal and 'RIGHT' or 'TOP', horizontal and -1 or 0, horizontal and 0 or -1)
+				tag:SetFormattedText('> %d', time)
+			else
+				tag:SetPoint('CENTER', self.bar, horizontal and 'LEFT' or 'BOTTOM', horizontal and (pos + endPadding) or 0, horizontal and 0 or (pos + endPadding))
+				tag:SetText(time)
+			end
+
+			tag:Show() -- show (or reshow) tag if set for appropriate time
+		end
+	end
+
+	-- configure control
+	durationMin	= cooldownDB.timeMinValue -- always a minimum time set to avoid the GCD
+	durationMax = (cooldownDB.timeMaxLimit) and cooldownDB.timeMaxValue or 2764800 -- 32 days, longer cooldown than that isn't going to be an issue...
+
+	EventRegistration('BAG_UPDATE_COOLDOWN',		cooldownDB.trackItem)
+	EventRegistration('PLAYER_EQUIPMENT_CHANGED',	cooldownDB.trackItem)
+	EventRegistration('PET_BAR_UPDATE_COOLDOWN',	cooldownDB.trackPet)
+	EventRegistration('SPELL_UPDATE_COOLDOWN',		cooldownDB.trackSpell)
+
+	if (onlyWhenTracking) then
+		if (activeTimersCount > 0) then
+			self:Show()
+		else
+			self:Hide()
+		end
+	else
+		self:Show()
+	end
+end
+
+function Cooldown:UpdateExistingTimers()
+	for _, timer in pairs(timerPool) do
+		timer:Configure()
+	end
+
+	local currentTime = GetTime()
+	local pos
+
+	for _, timer in pairs(activeTimers) do
+		timer:Configure()
+
+		if (not timer.expired) then -- don't bother with expiring timers, will be gone in <1s
+			if (timer.group == 'SPELL') then
+				timer.offset = (cooldownDB.offsetTags) and cooldownDB.offsetSpell or 0
+				timer.offsetTag:SetColorTexture(unpack(cooldownDB.colourSpell))
+				timer.border:SetVertexColor(unpack(cooldownDB.colourSpell))
+			elseif(timer.group == 'ITEM') then
+				timer.offset = (cooldownDB.offsetTags) and cooldownDB.offsetItem or 0
+				timer.offsetTag:SetColorTexture(unpack(cooldownDB.colourItem))
+				timer.border:SetVertexColor(unpack(cooldownDB.colourItem))
+			else -- group == 'PET'
+				timer.offset = (cooldownDB.offsetTags) and cooldownDB.offsetPet or 0
+				timer.offsetTag:SetColorTexture(unpack(cooldownDB.colourPet))
+				timer.border:SetVertexColor(unpack(cooldownDB.colourPet))
+			end
+
+			timer:SetWidth(thickness)
+			timer:SetHeight(thickness)
+
+			if (timer.offset ~= 0) then -- offset is enabled for this cooldown group
+				timer.offsetTag:ClearAllPoints()
+
+				if (horizontal) then
+					timer.offsetTag:SetPoint('TOP', timer, 'CENTER', 0, (timer.offset < 0) and (timer.offset * -1) or 0)
+					timer.offsetTag:SetPoint('BOTTOM', timer, 'CENTER', 0, (timer.offset > 0) and -(timer.offset) or 0)
+				else
+					timer.offsetTag:SetPoint('LEFT', timer, 'CENTER', (timer.offset > 0) and (timer.offset * -1) or 0, 0)
+					timer.offsetTag:SetPoint('RIGHT', timer, 'CENTER', (timer.offset < 0) and (timer.offset * -1) or 0, 0)
+				end
+
+				timer.offsetTag:Show()
+			else
+				timer.offsetTag:Hide()
+			end
+
+			pos = math_pow((timer.expireTime - currentTime) / maxTime, 0.4) * workLength
+
+			timer:ClearAllPoints() -- just to make sure our attachment point is clear before OnUpdate moves us
+
+			if (horizontal) then
+				timer:SetPoint('CENTER', Cooldown.bar, 'LEFT', endPadding + math_min(pos, workLength), timer.offset)
+			else
+				timer:SetPoint('CENTER', Cooldown.bar, 'BOTTOM', timer.offset, endPadding + math_min(pos, workLength))
+			end
+		end
+	end
+end
+
+function Cooldown:ApplyOptionsTimerRestrictions()
+	for _, timer in pairs(activeTimers) do
+		if (timer.duration <= durationMin or timer.duration >= durationMax) then
+			timer:Release()
+		else
+			if (timer.group == 'ITEM' and not cooldownDB.trackItem) then
+				timer:Release()
+			elseif (timer.group == 'PET' and not cooldownDB.trackPet) then
+				timer:Release()
+			elseif (timer.group == 'SPELL' and not cooldownDB.trackSpell) then
+				timer:Release()
+			end
+		end
+	end
+end
+
+
+-- ------------------------
+-- COOLDOWN TIMER SCRIPT HANDLERS
+-- ------------------------
+local function OnClick(self, button)
+	if (button == 'LeftButton') then
+		Ellipsis:Announce(self)
+	elseif (button == 'RightButton') then
+		if (IsShiftKeyDown()) then
+			Ellipsis:BlacklistCooldownAdd((self.group == 'ITEM') and 'ITEM' or 'SPELL', self.timerID)
+		else
+			self:Release()
+		end
+	end
+end
+
+local function OnEnter(self)
+	if (not self.isMouseOver) then
+		self.isMouseOver = true
+
+		GameTooltip:SetOwner(self, 'ANCHOR_BOTTOMLEFT')
+
+		if (cooldownDB.tooltips == 'FULL' and self.timerID > 0) then
+			if (self.group == 'ITEM') then
+				GameTooltip:SetItemByID(self.timerID)
+			else
+				GameTooltip:SetSpellByID(self.timerID)
+			end
+		else
+			GameTooltip:SetText(self.timerName, 1, 1, 1)
+		end
+
+		GameTooltip:AddLine(self.timerID > 0 and L.CooldownTimerTooltip or L.CooldownTimerTooltipNoBlock)
+		GameTooltip:Show()
+	end
+end
+
+local function OnLeave(self)
+	if (self.isMouseOver) then
+		self.isMouseOver = false
+
+		GameTooltip:Hide()
+	end
+end
+
+
+-- ------------------------
+-- COOLDOWN TIMER CREATION
+-- ------------------------
+local function CreateTimer()
+	local new = CreateFrame('Button', nil, Cooldown)
+	local widget
+
+	-- main gui widgets
+	widget = new:CreateTexture(nil, 'BORDER')
+	widget:SetAllPoints(new)
+	widget:SetTexCoord(0.08, 0.92, 0.08, 0.92)
+	new.icon = widget
+
+	widget = new:CreateTexture(nil, 'ARTWORK')
+	widget:SetAllPoints(new.icon)
+	widget:SetTexCoord(0.03125, 0.96875, 0.03125, 0.96875)
+	widget:SetTexture('Interface\\AddOns\\Ellipsis\\IconBorder')
+	new.border = widget
+
+	widget = new:CreateTexture(nil, 'BACKGROUND')
+	widget:SetWidth(1)
+	widget:SetHeight(1)
+	new.offsetTag = widget
+
+	new['Release']		= CooldownTimer.Release
+	new['Configure']	= CooldownTimer.Configure
+	new['SetExpired']	= CooldownTimer.SetExpired
+	new['Update']		= CooldownTimer.Update
+
+	return new
+end
+
+function CooldownTimer:New(currentTime, group, timerID, timerName, timerIcon, startTime, duration)
+	local new = tremove(timerPool, 1) -- grab an timer from the inactive pool (if any)
+
+	if (not new) then -- no inactive timers, create new
+		new = CreateTimer()
+		new:Configure()
+	end
+
+	new.updated		= currentTime
+	new.expired		= false			-- new timer, cannot be expired
+
+	new.startTime	= startTime
+	new.duration	= duration
+	new.expireTime	= startTime + duration
+
+	new.group		= group
+	new.timerID		= timerID
+	new.timerName	= timerName
+
+	if (group == 'SPELL') then
+		new.offset = (cooldownDB.offsetTags) and cooldownDB.offsetSpell or 0
+		new.offsetTag:SetColorTexture(unpack(cooldownDB.colourSpell))
+		new.border:SetVertexColor(unpack(cooldownDB.colourSpell))
+	elseif(group == 'ITEM') then
+		new.offset = (cooldownDB.offsetTags) and cooldownDB.offsetItem or 0
+		new.offsetTag:SetColorTexture(unpack(cooldownDB.colourItem))
+		new.border:SetVertexColor(unpack(cooldownDB.colourItem))
+	else -- group == 'PET'
+		new.offset = (cooldownDB.offsetTags) and cooldownDB.offsetPet or 0
+		new.offsetTag:SetColorTexture(unpack(cooldownDB.colourPet))
+		new.border:SetVertexColor(unpack(cooldownDB.colourPet))
+	end
+
+	new:SetWidth(thickness)
+	new:SetHeight(thickness)
+	new:SetAlpha(1)
+
+	new.icon:SetTexture(timerIcon)
+
+	if (new.offset ~= 0) then -- offset is enabled for this cooldown group
+		new.offsetTag:ClearAllPoints()
+
+		if (horizontal) then
+			new.offsetTag:SetPoint('TOP', new, 'CENTER', 0, (new.offset < 0) and (new.offset * -1) or 0)
+			new.offsetTag:SetPoint('BOTTOM', new, 'CENTER', 0, (new.offset > 0) and -(new.offset) or 0)
+		else
+			new.offsetTag:SetPoint('LEFT', new, 'CENTER', (new.offset > 0) and (new.offset * -1) or 0, 0)
+			new.offsetTag:SetPoint('RIGHT', new, 'CENTER', (new.offset < 0) and (new.offset * -1) or 0, 0)
+		end
+
+		new.offsetTag:Show()
+	else
+		new.offsetTag:Hide()
+	end
+
+	local pos = math.pow((new.expireTime - currentTime) / maxTime, 0.4) * workLength
+
+	new:ClearAllPoints() -- just to make sure our attachment point is clear before OnUpdate moves us
+
+	if (horizontal) then
+		new:SetPoint('CENTER', Cooldown.bar, 'LEFT', endPadding + math_min(pos, workLength), new.offset)
+	else
+		new:SetPoint('CENTER', Cooldown.bar, 'BOTTOM', new.offset, endPadding + math_min(pos, workLength))
+	end
+
+	new:Show()
+
+	activeTimers[group .. timerID] = new		-- add new timer to timer lookup
+	activeTimersCount = activeTimersCount + 1
+
+	if (onlyWhenTracking and activeTimersCount == 1) then -- first cooldown in a while, show bar
+		Cooldown:Show()
+	end
+end
+
+
+-- ------------------------
+-- COOLDOWN TIMER FUNCTIONS
+-- ------------------------
+function CooldownTimer:Release()
+	self:Hide()
+
+	activeTimers[self.group .. self.timerID] = nil	-- remove self from timer lookup
+
+	activeTimersCount = activeTimersCount - 1		-- decrement timer count
+
+	if (onlyWhenTracking and activeTimersCount == 0) then
+		Cooldown:Hide()
+	end
+
+	tinsert(timerPool, self) -- add self back into the timerPool (do last so we can't be used again before we're fully Released)
+end
+
+function CooldownTimer:Configure()
+	if (cooldownDB.interactive) then
+		self:EnableMouse(true)
+		self:SetScript('OnClick', OnClick)
+		self:RegisterForClicks('LeftButtonUp', 'RightButtonUp')
+
+		if (cooldownDB.tooltips ~= 'OFF') then
+			self:SetScript('OnEnter', OnEnter)
+			self:SetScript('OnLeave', OnLeave)
+		else
+			self:SetScript('OnEnter', nil)
+			self:SetScript('OnLeave', nil)
+		end
+	else
+		self:EnableMouse(false) -- non-interactive, disable all mouse capture
+	end
+end
+
+function CooldownTimer:SetExpired(currentTime)
+	if (self.expired) then return end -- already set as expired
+
+	currentTime = currentTime or GetTime() -- we need a time value, make sure we have one
+
+	if (sendAlerts) then
+		local premature = (currentTime + 0.5) < self.expireTime -- check to see if cooldown completed early (with a bit of slush time)
+
+		Ellipsis:AlertCooldown(premature, self) -- send alert if watching for alerts
+	end
+
+	self.expired	= true
+	self.expireTime	= currentTime -- make sure to update expiration for proper pulse duration
+end
+
+function CooldownTimer:Update(startTime, duration)
+	if (self.expired) then	-- still playing the expiration pulse, back on cooldown, reset to active status
+		self:SetWidth(thickness)
+		self:SetHeight(thickness)
+		self:SetAlpha(1)
+
+		self.expired = false
+	end
+
+	self.startTime	= startTime
+	self.duration	= duration
+	self.expireTime	= startTime + duration
+end
diff --git a/Ellipsis/Core.lua b/Ellipsis/Core.lua
index cb7dffb..bd5b2a2 100644
--- a/Ellipsis/Core.lua
+++ b/Ellipsis/Core.lua
@@ -13,11 +13,10 @@ local OPTIONS_ADDON			= AddonName .. '_Options'
 Ellipsis.NUM_AURA_ANCHORS	= 7

 Ellipsis.anchors			= {}	-- gui reference for unit anchors
-Ellipsis.auraPool			= {}	-- storage for inactive auras
-Ellipsis.unitPool			= {}	-- storage for inactive units
 Ellipsis.activeAuras		= {}	-- active auras for OnUpdate iteration and reference
 Ellipsis.activeUnits		= {}	-- active units for event reference
-
+Ellipsis.auraPool			= {}	-- storage for inactive auras
+Ellipsis.unitPool			= {}	-- storage for inactive units
 Ellipsis.anchorLookup		= {}	-- reference lookup for display of Units in the appropriate anchor for their group type
 Ellipsis.priorityLookup		= {}	-- reference lookup for priority sorting of Units on their anchors

@@ -53,6 +52,7 @@ function Ellipsis:OnInitialize()
 	self:InitializeAnchors()
 	self:InitializeAuras()
 	self:InitializeUnits()
+	self:InitializeCooldowns()
 	self:InitializeNotify()

 	self:RegisterChatCommand('ellipsis', 'ChatHandler')
@@ -91,11 +91,12 @@ function Ellipsis:UpdateVersion(major, minor, bugfix)
 	local newOptions = false


-	-- upgrade database and options
+	if (minor == '0') then -- addition of cooldown tracking
+		newOptions = true
+	end


-	-- finalize update
-	EllipsisVersion = CURRENT_VERSION
+	EllipsisVersion = CURRENT_VERSION -- finalize update

 	if (newOptions) then
 		self:Printf(L.VersionUpdatedNew, EllipsisVersion)
@@ -150,3 +151,29 @@ function Ellipsis:BlacklistRemove(spellID)
 		self:Printf(L.BlacklistRemove, name or L.BlacklistUnknown, spellID)
 	end
 end
+
+function Ellipsis:BlacklistCooldownAdd(group, timerID)
+	if (type(timerID) == 'number' and timerID > 0) then -- just ensure its a (potentially legitimate) timerID
+		self.db.profile.cooldowns.blacklist[group][timerID] = true
+
+		for _, timer in pairs(self.Cooldown.activeTimers) do
+			if (timer.timerID == timerID) then -- matching ID, matching group to?
+				if ((group == 'ITEM' and timer.group == 'ITEM') or (group ~=' ITEM' and timer.group ~= 'ITEM')) then
+					timer:Release() -- kill any active timers with this newly blocked ID
+				end
+			end
+		end
+
+		local name = (group == 'ITEM') and GetItemInfo(timerID) or GetSpellInfo(timerID)
+		self:Printf(L.BlacklistCooldownAdd, name or L.BlacklistUnknown, timerID)
+	end
+end
+
+function Ellipsis:BlacklistCooldownRemove(group, timerID)
+	if (self.db.profile.cooldowns.blacklist[group][timerID]) then
+		self.db.profile.cooldowns.blacklist[group][timerID] = nil
+
+		local name = (group == 'ITEM') and GetItemInfo(timerID) or GetSpellInfo(timerID)
+		self:Printf(L.BlacklistCooldownRemove, name or L.BlacklistUnknown, timerID)
+	end
+end
diff --git a/Ellipsis/Defaults.lua b/Ellipsis/Defaults.lua
index 039e930..77aea55 100644
--- a/Ellipsis/Defaults.lua
+++ b/Ellipsis/Defaults.lua
@@ -10,13 +10,14 @@ function Ellipsis:GetDefaults()
 		profile = {
 			locked			= false,	-- default to being unlocked for first install so anchors can be positioned
 			anchorData = { -- base display data for all anchors
-				[1] = {point = 'CENTER', x = 0, y = 128,  alpha = 1.0, scale = 1.0},
-				[2] = {point = 'CENTER', x = 0, y = 88,   alpha = 1.0, scale = 1.0},
-				[3] = {point = 'CENTER', x = 0, y = 48,   alpha = 1.0, scale = 1.0},
-				[4] = {point = 'CENTER', x = 0, y = 8,    alpha = 1.0, scale = 1.0},
-				[5] = {point = 'CENTER', x = 0, y = -32,  alpha = 1.0, scale = 1.0},
-				[6] = {point = 'CENTER', x = 0, y = -72,  alpha = 1.0, scale = 1.0},
-				[7] = {point = 'CENTER', x = 0, y = -112, alpha = 1.0, scale = 1.0},
+				[1]		= {point = 'CENTER', x = 0, y = 128,  alpha = 1.0, scale = 1.0},
+				[2]		= {point = 'CENTER', x = 0, y = 88,   alpha = 1.0, scale = 1.0},
+				[3]		= {point = 'CENTER', x = 0, y = 48,   alpha = 1.0, scale = 1.0},
+				[4]		= {point = 'CENTER', x = 0, y = 8,    alpha = 1.0, scale = 1.0},
+				[5]		= {point = 'CENTER', x = 0, y = -32,  alpha = 1.0, scale = 1.0},
+				[6]		= {point = 'CENTER', x = 0, y = -72,  alpha = 1.0, scale = 1.0},
+				[7]		= {point = 'CENTER', x = 0, y = -112, alpha = 1.0, scale = 1.0},
+				['CD']	= {point = 'CENTER', x = 0, y = -152, alpha = 1.0, scale = 1.0},
 			},
 			control = {
 				-- aura restrictions
@@ -95,6 +96,44 @@ function Ellipsis:GetDefaults()
 				colourFriendly		= {0, 1, 0, 1},
 				colourHostile		= {1, 0, 0, 1},
 			},
+			cooldowns = {
+				enabled				= false,
+				onlyWhenTracking	= false,
+				interactive			= true,			-- control ability to cancel/announce cooldowns with mouse-clicks
+				tooltips			= 'FULL',		-- FULL|HELPER|OFF
+				-- control
+				trackItem			= true,
+				trackPet			= true,
+				trackSpell			= true,
+				blacklist = {
+					['ITEM']		= {},			-- blacklisted item cooldowns
+					['SPELL']		= {},			-- blacklisted spell (pet and player) cooldowns
+				},
+				timeMinValue		= 2,			-- always enabled with a minimum greater than the GCD
+				timeMaxLimit		= false,
+				timeMaxValue		= 60,
+				-- appearance
+				horizontal			= true,
+				texture				= 'BantoBar',
+				length				= 200,
+				thickness			= 16,
+				timeDisplayMax		= 60,
+				timeDetailed		= false,
+				timeFont			= 'Friz Quadrata TT',
+				timeFontSize		= 10,
+				offsetTags			= false,
+				offsetItem			= 0,
+				offsetPet			= 0,
+				offsetSpell			= 0,
+				-- colours
+				colourBar			= {0.5, 0.5, 0.5, 1},
+				colourBackdrop		= {0, 0, 0, 1},
+				colourBorder		= {0, 0, 0, 1},
+				colourText			= {0.75, 0.75, 0.75, 1},
+				colourItem			= {0, 0, 1, 1},
+				colourPet			= {0, 1, 0, 1},
+				colourSpell			= {1, 0, 0, 1},
+			},
 			notify = {
 				outputAnnounce		= 'GROUPS',			-- AUTO|GROUPS|RAID|PARTY|SAY
 				outputAlerts		= {					-- storage for LibSink
@@ -102,11 +141,20 @@ function Ellipsis:GetDefaults()
 				},
 				-- aura alerts
 				auraBrokenAlerts	= false,
-				auraBrokenAudio		= 'Short Circuit',	-- an entry from LSM or 'none'
+				auraBrokenAudio		= 'Short Circuit',	-- an entry from LSM or 'None'
 				auraBrokenText		= true,
 				auraExpiredAlerts	= false,
-				auraExpiredAudio	= 'Simon Chime',	-- an entry from LSM or 'none'
+				auraExpiredAudio	= 'Simon Chime',	-- an entry from LSM or 'None'
 				auraExpiredText		= true,
+				-- cooldown alerts
+				coolPrematureAlerts	= false,
+				coolPrematureAudio	= 'Short Circuit',	-- an entry from LSM or 'None'
+				coolPrematureText	= true,
+				coolCompleteAlerts	= false,
+				coolCompleteAudio	= 'Simon Chime',	-- an entry from LSM or 'None'
+				coolCompleteText	= true,
+
+
 			},
 			advanced = {
 				tickRate			= 0.1,
diff --git a/Ellipsis/Ellipsis.toc b/Ellipsis/Ellipsis.toc
index 77af5bb..c2b8e8d 100644
--- a/Ellipsis/Ellipsis.toc
+++ b/Ellipsis/Ellipsis.toc
@@ -2,7 +2,7 @@
 ## Title: Ellipsis (|cff67b1e9K|cff4779ceith|cff67b1e9M|cff4779ceod|r)
 ## Notes: A full-featured, multi-target Aura (DoTs and HoTs) tracker.
 ## Author: Kith
-## Version: 4.0.1
+## Version: 4.1.0
 ## SavedVariables: EllipsisDB, EllipsisVersion
 ## OptionalDeps: Ace3, LibSharedMedia-3.0, LibSink-2.0
 ## X-Embeds: Ace3, LibSharedMedia-3.0, LibSink-2.0
@@ -21,12 +21,12 @@ Defaults.lua

 Anchor.lua
 Overlays.lua
-
 Aura.lua
 Unit.lua

 AuraData.lua
-
 Control.lua

-Notify.lua
\ No newline at end of file
+Cooldown.lua
+
+Notify.lua
diff --git a/Ellipsis/Locales/Local_enUS.lua b/Ellipsis/Locales/Local_enUS.lua
index ce17739..299095c 100644
--- a/Ellipsis/Locales/Local_enUS.lua
+++ b/Ellipsis/Locales/Local_enUS.lua
@@ -7,6 +7,10 @@
 local L = LibStub('AceLocale-3.0'):NewLocale('Ellipsis', 'enUS', true)


+L.OverlayCooldown = 'Cooldown Bar'
+
+
+

 L.VersionUpdated	= 'Version updated to v%s'
 L.VersionUpdatedNew	= 'Version updated to v%s - New settings are available!'
@@ -19,25 +23,34 @@ L.UnitLevel_Boss	= 'B'
 L.UnitName_NoTarget	= 'Non-Targeted'

 -- aura tooltips
-L.AuraTooltip			= '\n<Left Click> to announce duration\n<Right Click> to cancel aura timer\n<Shift-Right Click> to block this aura'
-L.AuraTooltipNoBlock	= '<Left Click> to announce duration\n<Right Click> to cancel aura timer\n|cffd0d0d0Can only block using Blacklist options|r'
+L.AuraTooltip			= '|cff67b1e9<Left Click> to announce duration\n<Right Click> to cancel aura timer\n<Shift-Right Click> to block this aura|r'
+L.AuraTooltipNoBlock	= '|cff67b1e9<Left Click> to announce duration\n<Right Click> to cancel aura timer|r\n|cffd0d0d0Can only block using Blacklist options|r'
+
+-- cooldown icon tooltips
+L.CooldownTimerTooltip			= '|cff67b1e9<Left Click> to announce cooldown\n<Right Click> to cancel cooldown timer\n<Shift-Right Click> to block this cooldown|r'
+L.CooldownTimerTooltipNoBlock	= '|cff67b1e9<Left Click> to announce cooldown\n<Right Click> to cancel cooldown timer|r\n|cffd0d0d0Can only block using Blacklist options|r'

 -- blacklisting
-L.BlacklistAdd		= 'Aura Added To The Blacklist: %s [|cffffd100%d|r]'
-L.BlacklistRemove	= 'Aura Removed From The Blacklist: %s [|cffffd100%d|r]'
-L.BlacklistUnknown	= 'Unknown Aura'
+L.BlacklistAdd				= 'Aura Added To The Blacklist: %s [|cffffd100%d|r]'
+L.BlacklistRemove			= 'Aura Removed From The Blacklist: %s [|cffffd100%d|r]'
+L.BlacklistCooldownAdd		= 'Cooldown Added To The Blacklist: %s [|cffffd100%d|r]'
+L.BlacklistCooldownRemove	= 'Cooldown Removed From The Blacklist: %s [|cffffd100%d|r]'
+L.BlacklistUnknown			= 'Unknown Aura'

 -- announcements
-L.Announce_ActiveAura			= 'My [%s] will expire on <%s> in %s.'
+L.Announce_ActiveAura			= 'My [%s] will expire on [%s] in %s.'
 L.Announce_ActiveAura_NoTarget	= 'My [%s] will expire in %s.'
-L.Announce_ExpiredAura			= 'My [%s] has expired on <%s>!'
+L.Announce_ExpiredAura			= 'My [%s] has expired on [%s]!'
 L.Announce_ExpiredAura_NoTarget	= 'My [%s] has expired!'
-L.Announce_PassiveAura			= 'My [%s] is active on <%s>.'
+L.Announce_PassiveAura			= 'My [%s] is active on [%s].'
 L.Announce_PassiveAura_NoTarget	= 'My [%s] is active.'
+L.Announce_ActiveCooldown		= 'My [%s] is on cooldown for %s.'

 -- alerts
-L.Alert_ExpiredAura	= '%s has EXPIRED on %s!'
-L.Alert_BrokenAura	= '%s has BROKEN on %s!'
+L.Alert_ExpiredAura		= '%s has EXPIRED on %s!'
+L.Alert_BrokenAura		= '%s has BROKEN on %s!'
+L.Alert_PrematureCool	= '%s cooldown completed early!'
+L.Alert_CompleteCool	= '%s cooldown has completed!'

 -- overlay strings
 L.OverlayHelperText		= 'Frames unlocked for positioning, scaling and opacity changes. Click below for full options or Exit to lock frame positions and hide overlays.'
diff --git a/Ellipsis/Notify.lua b/Ellipsis/Notify.lua
index 441da65..1689f07 100644
--- a/Ellipsis/Notify.lua
+++ b/Ellipsis/Notify.lua
@@ -14,8 +14,10 @@ local PlaySoundFile, SendChatMessage = PlaySoundFile, SendChatMessage
 local notifyDB

 local alertLastPlayed = {
-	['auraBroken']	= 0,
-	['auraExpired']	= 0,
+	['auraBroken']		= 0,
+	['auraExpired']		= 0,
+	['coolPremature']	= 0,
+	['coolComplete']	= 0
 }


@@ -42,7 +44,7 @@ local function SecondsToTime_Abrv(time)
 	end
 end

-function Ellipsis:Announce(aura)
+function Ellipsis:Announce(object)
 	local output = notifyDB.outputAnnounce
 	local channel

@@ -58,30 +60,38 @@ function Ellipsis:Announce(aura)
 		end
 	end

-	if (channel) then -- a valid channel is available to announce, proceed
-		if (aura.expired) then
-			if (aura.parentUnit.group == 'notarget') then -- was an aoe, or other global aura, not targeted
-				SendChatMessage(format(L.Announce_ExpiredAura_NoTarget, aura.spellName), channel)
+	if (not channel) then return end -- no valid channale is available, abort
+
+	if (object.auraID) then -- this is an aura
+		if (object.expired) then
+			if (object.parentUnit.group == 'notarget') then -- was an aoe, or other global aura, not targeted
+				SendChatMessage(format(L.Announce_ExpiredAura_NoTarget, object.spellName), channel)
 			else
-				SendChatMessage(format(L.Announce_ExpiredAura, aura.spellName, aura.parentUnit.unitName), channel)
+				SendChatMessage(format(L.Announce_ExpiredAura, object.spellName, object.parentUnit.unitName), channel)
 			end
 		else -- active aura
-			local remains = format(SecondsToTime_Abrv(aura.expireTime - GetTime())) -- co-opting the abbreviated time display from auras for announcements
+			local remains = format(SecondsToTime_Abrv(object.expireTime - GetTime())) -- co-opting the abbreviated time display from auras for announcements

-			if (aura.parentUnit.group == 'notarget') then -- is an aoe, or other global aura, not targeted
-				if (aura.duration == 0) then -- passive
-					SendChatMessage(format(L.Announce_PassiveAura_NoTarget, aura.spellName), channel)
+			if (object.parentUnit.group == 'notarget') then -- is an aoe, or other global aura, not targeted
+				if (object.duration == 0) then -- passive
+					SendChatMessage(format(L.Announce_PassiveAura_NoTarget, object.spellName), channel)
 				else
-					SendChatMessage(format(L.Announce_ActiveAura_NoTarget, aura.spellName, remains), channel)
+					SendChatMessage(format(L.Announce_ActiveAura_NoTarget, object.spellName, remains), channel)
 				end
 			else
-				if (aura.duration == 0) then -- passive
-					SendChatMessage(format(L.Announce_PassiveAura, aura.spellName, aura.parentUnit.unitName), channel)
+				if (object.duration == 0) then -- passive
+					SendChatMessage(format(L.Announce_PassiveAura, object.spellName, object.parentUnit.unitName), channel)
 				else
-					SendChatMessage(format(L.Announce_ActiveAura, aura.spellName, aura.parentUnit.unitName, remains), channel)
+					SendChatMessage(format(L.Announce_ActiveAura, object.spellName, object.parentUnit.unitName, remains), channel)
 				end
 			end
 		end
+	else -- not an aura, must be a cooldown timer
+		if (object.expired) then return end -- no need to announce an expired cooldown timer
+
+		local remains = format(SecondsToTime_Abrv(object.expireTime - GetTime())) -- co-opting the abbreviated time display from auras for announcements
+
+		SendChatMessage(format(L.Announce_ActiveCooldown, object.timerName, remains), channel)
 	end
 end

@@ -126,3 +136,39 @@ function Ellipsis:AlertAura(isBroken, aura)
 		end
 	end
 end
+
+function Ellipsis:AlertCooldown(isPremature, timer)
+	if (isPremature) then
+		if (notifyDB.coolPrematureAlerts) then
+			if (notifyDB.coolPrematureAudio ~= 'None') then -- play audio for premature cooldown
+				local time = GetTime()
+
+				if ((time - alertLastPlayed['coolPremature']) > 0.25) then -- only play if its been a short time since the last audio alert
+					alertLastPlayed['coolPremature'] = time
+
+					PlaySoundFile(LSM:Fetch('sound', notifyDB.coolPrematureAudio), 'Master')
+				end
+			end
+
+			if (notifyDB.coolPrematureText) then -- show message for premature cooldowns
+				self:Pour(format(L.Alert_PrematureCool, timer.timerName))
+			end
+		end
+	else -- completed
+		if (notifyDB.coolCompleteAlerts) then
+			if (notifyDB.coolCompleteAudio ~= 'None') then -- play audio for complete cooldown
+				local time = GetTime()
+
+				if ((time - alertLastPlayed['coolComplete']) > 0.25) then -- only play if its been a short time since the last audio alert
+					alertLastPlayed['coolComplete'] = time
+
+					PlaySoundFile(LSM:Fetch('sound', notifyDB.coolCompleteAudio), 'Master')
+				end
+			end
+
+			if (notifyDB.coolCompleteText) then -- show message for complete cooldowns
+				self:Pour(format(L.Alert_CompleteCool, timer.timerName))
+			end
+		end
+	end
+end
diff --git a/Ellipsis/Overlays.lua b/Ellipsis/Overlays.lua
index a015453..796aaee 100644
--- a/Ellipsis/Overlays.lua
+++ b/Ellipsis/Overlays.lua
@@ -55,25 +55,32 @@ end

 local function SetTooltip(self)
 	GameTooltip:SetOwner(self, 'ANCHOR_BOTTOMLEFT')
-	GameTooltip:SetText(format(L.OverlayTooltipHeader, self.owner.anchorID), 1, 1, 1)
-	GameTooltip:AddLine(format(L.OverlayTooltipHelp, ceil(self.owner:GetAlpha() * 100), self.owner:GetScale()))

-	local anchorID	= self.owner.anchorID
-	unitGroupsShown	= wipe(unitGroupsShown) -- cleanse display table
+	if (self.owner.anchorID == 'CD') then -- special case for the Cooldown Bar
+		GameTooltip:SetText(L.OverlayCooldown, 1, 1, 1)
+		GameTooltip:AddLine(format(L.OverlayTooltipHelp, ceil(self.owner:GetAlpha() * 100), self.owner:GetScale()))
+	else
+		GameTooltip:SetText(format(L.OverlayTooltipHeader, self.owner.anchorID), 1, 1, 1)
+		GameTooltip:AddLine(format(L.OverlayTooltipHelp, ceil(self.owner:GetAlpha() * 100), self.owner:GetScale()))
+
+		local anchorID	= self.owner.anchorID
+		unitGroupsShown	= wipe(unitGroupsShown) -- cleanse display table

-	for group, anchor in pairs(Ellipsis.anchorLookup) do
-		if (anchor and anchor.anchorID == anchorID) then -- this unitGroup is shown by this anchor
-			tinsert(unitGroupsShown, format('|cff67b1e9%s|r', L['UnitGroup_' .. group]))
+		for group, anchor in pairs(Ellipsis.anchorLookup) do
+			if (anchor and anchor.anchorID == anchorID) then -- this unitGroup is shown by this anchor
+				tinsert(unitGroupsShown, format('|cff67b1e9%s|r', L['UnitGroup_' .. group]))
+			end
 		end
-	end

-	if (#unitGroupsShown == 0) then
-		tinsert(unitGroupsShown, format('|cff67b1e9%s|r', L.UnitGroup_none))
-	else
-		table.sort(unitGroupsShown)
+		if (#unitGroupsShown == 0) then
+			tinsert(unitGroupsShown, format('|cff67b1e9%s|r', L.UnitGroup_none))
+		else
+			table.sort(unitGroupsShown)
+		end
+
+		GameTooltip:AddLine(format(L.OverlayTooltipAuras, strjoin(', ', unpack(unitGroupsShown))), nil, nil, nil, true)
 	end

-	GameTooltip:AddLine(format(L.OverlayTooltipAuras, strjoin(', ', unpack(unitGroupsShown))), nil, nil, nil, true)
 	GameTooltip:Show()
 end

@@ -181,7 +188,7 @@ local function CreateOverlay(owner)
 	f:SetBackdrop(dropOverlay)

 	f:SetNormalFontObject('GameFontNormalLarge')
-	f:SetText(owner.anchorID)
+	f:SetText(owner.anchorID == 'CD' and L.OverlayCooldown or owner.anchorID)

 	f:SetScript('OnMouseDown',	OnMouseDown)
 	f:SetScript('OnMouseUp',	OnMouseUp)
@@ -265,6 +272,8 @@ function Ellipsis:UnlockInterface()
 		for id, anchor in pairs(self.anchors) do
 			self.overlays[id] = CreateOverlay(anchor)
 		end
+
+		self.overlays['CD'] = CreateOverlay(self.Cooldown)
 	end

 	for _, overlay in pairs(self.overlays) do
diff --git a/Ellipsis_Options/CooldownOptions.lua b/Ellipsis_Options/CooldownOptions.lua
index 3ec040d..e09501c 100644
--- a/Ellipsis_Options/CooldownOptions.lua
+++ b/Ellipsis_Options/CooldownOptions.lua
@@ -2,13 +2,168 @@ local Ellipsis = _G['Ellipsis']
 local L			= LibStub('AceLocale-3.0'):GetLocale('Ellipsis_Options')
 local LSM		= LibStub('LibSharedMedia-3.0')

+local blacklistCooldownToAdd	= false
+local blacklistCooldownIsItem	= false
+local blacklistCooldownToRemove	= false
+local blacklistCooldownList		= {}
+
+local function GetBlacklistedCooldowns()
+	blacklistCooldownList = wipe(blacklistCooldownList)
+
+	local name
+
+	for spellID in pairs(Ellipsis.db.profile.cooldowns.blacklist.SPELL) do
+		name = GetSpellInfo(spellID)
+		name = name or ''
+		blacklistCooldownList['SPELL_' .. spellID] = format('%s [|cffffd100%d|r] %s', L.CoolSpell, spellID, name)
+	end
+
+	for itemID in pairs(Ellipsis.db.profile.cooldowns.blacklist.ITEM) do
+		name = GetItemInfo(itemID)
+		name = name or ''
+		blacklistCooldownList['ITEM_' .. itemID] = format('%s [|cffffd100%d|r] %s', L.CoolItem, itemID, name)
+	end
+
+	return blacklistCooldownList
+end
+
+
+local dropTooltips = {
+	['FULL']		= L.AuraDropTooltip_FULL,
+	['HELPER']		= L.AuraDropTooltip_HELPER,
+	['OFF']			= L.AuraDropTooltip_OFF,
+}
+
 local cooldownOptions = {
 	appearance = {
-		name = L.CooldownAppearance,
+		name = L.Appearance,
 		type = 'group',
 		order = -2,
 		args = {
-			-- appearance options
+			horizontal = {
+				name = L.CoolHorizontal,
+				desc = L.CoolHorizontalDesc,
+				type = 'toggle',
+				order = 1,
+			},
+			texture = {
+				name = L.CoolTexture,
+				desc = L.CoolTextureDesc,
+				type = 'select',
+				order = 2,
+				values = LSM:HashTable('statusbar'),
+				dialogControl = 'LSM30_Statusbar',
+			},
+			length = {
+				name = L.CoolLength,
+				desc = L.CoolLengthDesc,
+				type = 'range',
+				order = 3,
+				min = 100,
+				max = 500,
+				step = 1,
+				bigStep = 5,
+			},
+			thickness = {
+				name = L.CoolThickness,
+				desc = L.CoolThicknessDesc,
+				type = 'range',
+				order = 4,
+				min = 12,
+				max = 40,
+				step = 1,
+			},
+			timeGroup = {
+				name = L.CoolTimeHeader,
+				type = 'group',
+				inline = true,
+				order = 5,
+				args = {
+					timeDisplayMax = {
+						name = L.CoolTimeDisplayMax,
+						desc = L.CoolTimeDisplayMaxDesc,
+						type = 'range',
+						order = 1,
+						min = 60,
+						max = 1800,
+						step = 10,
+						bigStep = 30,
+					},
+					timeDetailed = {
+						name = L.CoolTimeDetailed,
+						desc = L.CoolTimeDetailedDesc,
+						type = 'toggle',
+						order = 2,
+					},
+					timeFont = {
+						name = L.CoolTimeFont,
+						desc = L.CoolTimeFontDesc,
+						type = 'select',
+						order = 3,
+						values = LSM:HashTable('font'),
+						dialogControl = 'LSM30_Font',
+					},
+					timeFontSize = {
+						name = L.CoolTimeFontSize,
+						desc = L.CoolTimeFontDesc,
+						type = 'range',
+						order = 4,
+						min = 4,
+						max = 24,
+						step = 1,
+					},
+				}
+			},
+			offsetGroup = {
+				name = '',--L.CoolOffsetHeader,
+				type = 'group',
+				inline = true,
+				order = 6,
+				args = {
+					offsetTags = {
+						name = L.CoolOffsetTags,
+						desc = L.CoolOffsetTagsDesc,
+						type = 'toggle',
+						order = 1,
+					},
+					offsetItem = {
+						name = L.CoolOffsetItem,
+						desc = L.CoolOffsetDesc,
+						type = 'range',
+						order = 2,
+						min = -60,
+						max = 60,
+						step = 1,
+						disabled = function()
+							return not Ellipsis.db.profile.cooldowns.offsetTags
+						end,
+					},
+					offsetPet = {
+						name = L.CoolOffsetPet,
+						desc = L.CoolOffsetDesc,
+						type = 'range',
+						order = 3,
+						min = -60,
+						max = 60,
+						step = 1,
+						disabled = function()
+							return not Ellipsis.db.profile.cooldowns.offsetTags
+						end,
+					},
+					offsetSpell = {
+						name = L.CoolOffsetSpell,
+						desc = L.CoolOffsetDesc,
+						type = 'range',
+						order = 4,
+						min = -60,
+						max = 60,
+						step = 1,
+						disabled = function()
+							return not Ellipsis.db.profile.cooldowns.offsetTags
+						end,
+					},
+				}
+			},
 		}
 	},
 	colours = {
@@ -16,10 +171,262 @@ local cooldownOptions = {
 		type = 'group',
 		order = -1,
 		args = {
-			-- colour options
+			groupBase = {
+				name = L.CoolColoursBase,
+				type = 'group',
+				inline = true,
+				order = 1,
+				args = {
+					colourBar = {
+						name = L.CoolColoursBar,
+						type = 'color',
+						order = 1,
+						hasAlpha = true,
+					},
+					colourText = {
+						name = L.CoolColoursText,
+						type = 'color',
+						order = 2,
+						hasAlpha = true,
+					},
+					colourBackdrop = {
+						name = L.CoolColoursBackdrop,
+						desc = L.CoolColoursBackdropDesc,
+						type = 'color',
+						order = 3,
+						hasAlpha = true,
+					},
+					colourBorder = {
+						name = L.CoolColoursBorder,
+						desc = L.CoolColoursBorderDesc,
+						type = 'color',
+						order = 4,
+						hasAlpha = true,
+					},
+
+				}
+			},
+			groupGroups = {
+				name = L.CoolColoursGroups,
+				type = 'group',
+				inline = true,
+				order = 2,
+				args = {
+					colourItem = {
+						name = L.CoolTrackItem,
+						desc = L.CoolColoursGroupsDesc,
+						type = 'color',
+						order = 1,
+						hasAlpha = true,
+					},
+					colourPet = {
+						name = L.CoolTrackPet,
+						desc = L.CoolColoursGroupsDesc,
+						type = 'color',
+						order = 2,
+						hasAlpha = true,
+					},
+					colourSpell = {
+						name = L.CoolTrackSpell,
+						desc = L.CoolColoursGroupsDesc,
+						type = 'color',
+						order = 3,
+						hasAlpha = true,
+					},
+				}
+			}
 		}
-	}
-	-- the rest
+	},
+	enabled = {
+		name = L.Enabled,
+		type = 'toggle',
+		order = 1,
+	},
+	onlyWhenTracking = {
+		name = L.CoolOnlyWhenTracking,
+		desc = L.CoolOnlyWhenTrackingDesc,
+		type = 'toggle',
+		order = 2,
+	},
+	interactive = {
+		name = L.CoolInteractive,
+		desc = L.CoolInteractiveDesc,
+		type = 'toggle',
+		order = 3,
+	},
+	tooltips = {
+		name = L.CoolTooltips,
+		desc = L.CoolTooltipsDesc,
+		type = 'select',
+		order = 4,
+		values = dropTooltips,
+		width = 'half',
+		disabled = function()
+			return not Ellipsis.db.profile.cooldowns.interactive
+		end,
+	},
+	groupControl = {
+		name = L.CoolControlHeader,
+		type = 'group',
+		inline = true,
+		order = 5,
+		args = {
+			trackingHeader = {
+				name = L.CoolTrackingHeader,
+				type = 'description',
+				order = 1,
+				width = 'half',
+				fontSize = 'medium',
+			},
+			trackItem = {
+				name = L.CoolTrackItem,
+				desc = L.CoolTrackItemDesc,
+				type = 'toggle',
+				order = 2,
+				width = 'half',
+			},
+			trackPet = {
+				name = L.CoolTrackPet,
+				desc = L.CoolTrackPetDesc,
+				type = 'toggle',
+				order = 3,
+				width = 'half',
+			},
+			trackSpell = {
+				name = L.CoolTrackSpell,
+				desc = L.CoolTrackSpellDesc,
+				type = 'toggle',
+				order = 4,
+				width = 'half',
+			},
+			timeMinLimit = {
+				name = L.CoolTimeMinLimit,
+				type = 'toggle',
+				order = 5,
+				disabled = true,
+				get = function() return true end, -- is a fake, always set to enabled
+			},
+			timeMaxLimit = {
+				name = L.CoolTimeMaxLimit,
+				type = 'toggle',
+				order = 6,
+			},
+			timeMinValue = {
+				name = L.CoolTimeMinValue,
+				desc = L.CoolTimeMinValueDesc,
+				type = 'range',
+				order = 7,
+				min = 2,
+				max = 60,
+				step = 1,
+			},
+			timeMaxValue = {
+				name = L.CoolTimeMaxValue,
+				desc = L.CoolTimeMaxValueDesc,
+				type = 'range',
+				order = 8,
+				min = 10,
+				max = 300,
+				step = 1,
+				bigStep = 10,
+				disabled = function()
+					return not Ellipsis.db.profile.cooldowns.timeMaxLimit
+				end
+			},
+			groupAdd = {
+				name = '',
+				type = 'group',
+				inline = true,
+				order = 9,
+				args = {
+					addInput = {
+						name = L.CoolBlacklistAdd,
+						desc = L.CoolBlacklistAddDesc,
+						type = 'input',
+						order = 1,
+						multiline = false,
+						get = function()
+							if (blacklistCooldownToAdd) then
+								return tostring(blacklistCooldownToAdd)
+							else
+								return ''
+							end
+						end,
+						set = function(info, val)
+							val = tonumber(val)
+
+							if (val and val > 0) then
+								blacklistCooldownToAdd = val
+							end
+						end
+					},
+					addIsItem = {
+						name = L.CoolBlacklistAddItem,
+						desc = L.CoolBlacklistAddItemDesc,
+						type = 'toggle',
+						order = 2,
+						width = 'half',
+						get = function()
+							return blacklistCooldownIsItem
+						end,
+						set = function(info, val)
+							blacklistCooldownIsItem = val
+						end,
+					},
+					addExecute = {
+						name = L.CoolBlacklistAddButton,
+						type = 'execute',
+						order = 3,
+						width = 'half',
+						func = function()
+							Ellipsis:BlacklistCooldownAdd(blacklistCooldownIsItem and 'ITEM' or 'SPELL', blacklistCooldownToAdd)
+
+							blacklistCooldownToAdd = false -- ID added, clear
+						end,
+						disabled = function() -- only enable if a valid ID is waiting to be added
+							return not blacklistCooldownToAdd
+						end
+					},
+				}
+			},
+			blacklistList = {
+				name = L.CoolBlacklistList,
+				desc = L.CoolBlacklistListDesc,
+				type = 'select',
+				order = 10,
+				width = 'full',
+				values = GetBlacklistedCooldowns,
+				get = function()
+					if (blacklistCooldownToRemove) then
+						return blacklistCooldownToRemove
+					else
+						return nil
+					end
+				end,
+				set = function(info, val)
+					blacklistCooldownToRemove = val
+				end
+			},
+			blacklistRemove = {
+				name = L.CoolBlacklistRemoveButton,
+				type = 'execute',
+				order = 11,
+				width = 'full',
+				func = function()
+					local group, timerID = strsplit('_', blacklistCooldownToRemove)
+
+					timerID = tonumber(timerID) or nil
+
+					Ellipsis:BlacklistCooldownRemove(group, timerID)
+
+					blacklistCooldownToRemove = false -- ID removed, clear
+				end,
+				disabled = function () -- only allow removable once a valid ID has been chosen first
+					return not blacklistCooldownToRemove
+				end
+			},
+		}
+	},
 }

 -- ------------------------
@@ -28,3 +435,35 @@ local cooldownOptions = {
 function Ellipsis:GetCooldownOptions()
 	return cooldownOptions
 end
+
+
+-- ------------------------
+-- GETTERS & SETTERS
+-- ------------------------
+function Ellipsis:CooldownsGet(info)
+	if (info.type == 'color') then -- special case for colour options
+		local colours = self.db.profile.cooldowns[info[#info]]
+
+		return colours[1], colours[2], colours[3], colours[4]
+	else
+		return self.db.profile.cooldowns[info[#info]]
+	end
+end
+
+function Ellipsis:CooldownsSet(info, val, val2, val3, val4)
+	if (info.type == 'color') then -- special case for colour options
+		local colours = self.db.profile.cooldowns[info[#info]]
+
+		if (info.option.hasAlpha) then -- setting alpha value as well as rgb
+			colours[1], colours[2], colours[3], colours[4] = val, val2, val3, val4
+		else -- no alpha, just rgb
+			colours[1], colours[2], colours[3] = val, val2, val3
+		end
+	else
+		self.db.profile.cooldowns[info[#info]] = val
+	end
+
+	Ellipsis.Cooldown:Configure()
+	Ellipsis.Cooldown:ApplyOptionsTimerRestrictions()
+	Ellipsis.Cooldown:UpdateExistingTimers()
+end
diff --git a/Ellipsis_Options/Ellipsis_Options.toc b/Ellipsis_Options/Ellipsis_Options.toc
index d2f38b1..600f461 100644
--- a/Ellipsis_Options/Ellipsis_Options.toc
+++ b/Ellipsis_Options/Ellipsis_Options.toc
@@ -2,7 +2,7 @@
 ## Title: Ellipsis Options (|cff67b1e9K|cff4779ceith|cff67b1e9M|cff4779ceod|r)
 ## Notes: Options for Ellipsis. Must be enabled to alter settings.
 ## Author: Kith
-## Version: 4.0.0
+## Version: 4.1.0
 ## RequiredDeps: Ellipsis
 ## OptionalDeps: Ace3
 ## LoadOnDemand: 1
diff --git a/Ellipsis_Options/Locales/Local_enUS.lua b/Ellipsis_Options/Locales/Local_enUS.lua
index e9fc76d..a0e53c3 100644
--- a/Ellipsis_Options/Locales/Local_enUS.lua
+++ b/Ellipsis_Options/Locales/Local_enUS.lua
@@ -11,6 +11,7 @@ local L = LibStub('AceLocale-3.0'):NewLocale('Ellipsis_Options', 'enUS', true)
 -- GENERIC STRINGS
 -- ------------------------
 L.Enabled				= 'Enabled'
+L.Appearance			= 'Appearance'
 L.Colours				= 'Colours'


@@ -24,7 +25,7 @@ L.ProfileReset		= 'Reset Profile: %s'


 -- ------------------------
--- EXAMPLE AURAS
+-- EXAMPLE AURAS & COOLDOWNS
 -- ------------------------
 L.SampleUnitHarmful = 'Hostile Unit'
 L.SampleUnitHelpful	= 'Friendly Unit'
@@ -33,7 +34,9 @@ L.SampleAuraBuff	= 'Sample Buff %d'
 L.SampleAuraMinion	= 'Sample Minion'
 L.SampleAuraTotem	= 'Sample Totem'
 L.SampleAuraGTAoE	= 'Sample Ground AoE'
-
+L.SampleCoolItem	= 'Sample Item Cooldown'
+L.SampleCoolPet		= 'Sample Pet Cooldown'
+L.SampleCoolSpell	= 'Sample Spell Cooldown'

 -- ------------------------
 -- BASE OPTIONS (Options.lua)
@@ -44,7 +47,7 @@ L.GeneralControl2Header	= 'Grouping & Tracking'
 L.GeneralControl3Header	= 'Layout & Sorting'
 L.AuraHeader			= 'Aura Configuration'
 L.UnitHeader		 	= 'Unit Configuration'
-L.CooldownHeader		= 'Cooldown Options [NYI]'
+L.CooldownHeader		= 'Cooldown Options'
 L.NotifyHeader			= 'Notification Options'
 L.AdvancedHeader		= '|cffff8040Advanced Settings'

@@ -232,7 +235,77 @@ L.UnitCollapseNoTargetDesc	= 'Set whether to collapse the header (set height to
 -- ------------------------
 -- COOLDOWN OPTIONS (CooldownOptions.lua)
 -- ------------------------
-L.CooldownAppearance = 'Appearance'
+L.CoolItem	= 'ITEM'
+L.CoolSpell	= 'SPELL'
+
+L.CoolOnlyWhenTracking		= 'Only When Tracking'
+L.CoolOnlyWhenTrackingDesc	= 'Set the cooldown bar to only be visible when there is a cooldown being tracked.\n\nWhen disabled, the cooldown bar will always be visible.'
+L.CoolInteractive			= 'Interactive'
+L.CoolInteractiveDesc		= 'Allow individual cooldown timers to be announced, cancelled or blacklisted by mouse interaction.\n\nDisabling this options allows you to click-through cooldown timers and select the world behind them. The cooldown bar itself always allows click-through.'
+L.CoolTooltips				= 'Show Tooltips'
+L.CoolTooltipsDesc			= 'Set how tooltips should be displayed when interacting with auras.\n\nFull:\nShow aura info and helper comments\n\nHelper:\nShow only helper comments\n\nOff:\nDo not display tooltips'
+
+L.CoolControlHeader			= 'Cooldown Tracking & Control'
+L.CoolTrackingHeader		= 'Tracking:'
+L.CoolTrackItem				= 'Items'
+L.CoolTrackItemDesc			= 'Enable tracking of item cooldowns.\n\nIncludes both equipped items as well as those in your bags such as potions. Does not include toys.'
+L.CoolTrackPet				= 'Pets'
+L.CoolTrackPetDesc			= 'Enable tracking of your pet\'s ability cooldowns.\n\nThis only includes your actual pet and not temporary minion summons.'
+L.CoolTrackSpell			= 'Spells'
+L.CoolTrackSpellDesc		= 'Enable tracking of your ability and spell cooldowns.\n\nThis includes General abilities such as Revive Battle Pets unless blacklisted.'
+
+L.CoolTimeMinLimit			= 'Limit Minimum'
+L.CoolTimeMinValue			= 'Minimum Duration'
+L.CoolTimeMinValueDesc		= 'Set the minimum duration (in seconds) a cooldown can have before it is blocked from display. All cooldowns with a duration less than, or equal to, this value will not be displayed.\n\nMinimum duration cannot be disabled and will always have a minimum of 2 seconds to prevent EVERY ability (on the GCD) from being tracked everytime an ability is used.'
+L.CoolTimeMaxLimit			= 'Limit Maximum'
+L.CoolTimeMaxValue			= 'Maximum Duration'
+L.CoolTimeMaxValueDesc		= 'Set the maximum duration (in seconds) a cooldown can have before it is blocked from display. All cooldowns with a duration greater than, or equal to, this value will not be displayed.'
+
+L.CoolBlacklistAdd			= 'Cooldown To Blacklist'
+L.CoolBlacklistAddDesc		= 'Cooldowns must be blacklisted by either SpellID or their ItemID rather than their name. For help finding the ID associated to a spell or item, you can use the databases on these sites:\n |cffffd100http://www.wowhead.com|r\n |cffffd100http://www.wowdb.com|r\n\nAlternatively, if cooldowns are set to be Interactive, you can blacklist them directy by using <Shift-Right Click> on the cooldown timer itself.'
+L.CoolBlacklistAddItem		= 'Is ItemID'
+L.CoolBlacklistAddItemDesc	= 'Set whether the ID being blacklisted is an ItemID as opposed to a SpellID.'
+L.CoolBlacklistAddButton	= 'Blacklist'
+L.CoolBlacklistList			= 'Blacklisted Cooldowns'
+L.CoolBlacklistListDesc		= 'This is a list of all cooldowns currently blacklisted from display, ordered first by whether they are items or spells, then by their ItemID or SpellID.\n\nCooldowns can be removed from the list by selecting them and using the button below.'
+L.CoolBlacklistRemoveButton	= 'Remove Cooldown From Blacklist'
+
+L.CoolHorizontal			= 'Horizontal Bar'
+L.CoolHorizontalDesc		= 'Set the orientation of the cooldown bar.\n\nWhen horizontal, cooldowns will countdown towards the left of the bar and towards the bottom when vertical.'
+L.CoolTexture				= 'Bar Texture'
+L.CoolTextureDesc			= 'Set the texture to be used for the cooldown bar. This will be outlined by the backdrop colour as chosen under \n|cffffd100Colours|r.'
+L.CoolLength				= 'Bar Length'
+L.CoolLengthDesc			= 'Set the largest dimension for the cooldown bar. When horizontal, this will be its width, and its height when vertical.'
+L.CoolThickness				= 'Bar Thickness'
+L.CoolThicknessDesc			= 'Set the smallest dimension for the cooldown bar. When horizontal, this will be its height, and its width when vertical.\n\nThis also sets the size of the cooldown timers themselves.'
+
+L.CoolTimeHeader			= 'Timescale & Time Tags'
+L.CoolTimeDisplayMax		= 'Timescale Displayed'
+L.CoolTimeDisplayMaxDesc	= 'Set the amount of time displayed by the cooldown bar.\n\nAny cooldowns with a duration greater than this value will be anchored to the end of the bar until they are able to start counting down.'
+L.CoolTimeDetailed			= 'Detailed Timescale'
+L.CoolTimeDetailedDesc		= 'Set whether to add extra time tags to the timescale display.\n\nThis provides more information as to where cooldowns are on the timescale, but can become overly crowded with higher timescales and short bars.'
+L.CoolTimeFont				= 'Time Tag Font'
+L.CoolTimeFontSize			= 'Time Tag Font Size'
+L.CoolTimeFontDesc			= 'Set the font, and the size of the font, to use for display of the time tags on the cooldown bar.'
+
+L.CoolOffsetHeader			= 'Offset Anchors'
+L.CoolOffsetTags			= 'Enable Offset Timers'
+L.CoolOffsetTagsDesc		= 'Set whether to allow timers to be offset away from the bar itself and attached by a \'spoke\' back to its position on the bar.'
+L.CoolOffsetItem			= 'Offset: Items'
+L.CoolOffsetPet				= 'Offset: Pets'
+L.CoolOffsetSpell			= 'Offset: Spells'
+L.CoolOffsetDesc			= 'Set the offset distance for this cooldown group. A setting of 0 will disable the offset for this group.'
+
+L.CoolColoursBase			= 'Base Cooldown Colours'
+L.CoolColoursBar			= 'Cooldown Bar'
+L.CoolColoursBackdrop		= 'Bar Backdrop'
+L.CoolColoursBackdropDesc	= 'Set the colour of the cooldown bar\'s backdrop. This is only visible if the colour of the bar itself is set to be transparent.'
+L.CoolColoursBorder			= 'Bar Border'
+L.CoolColoursBorderDesc		= 'Set the colour of the cooldown bar\'s border. This is the outline seen around the cooldown bar itself.'
+L.CoolColoursText			= 'Timescale Text'
+
+L.CoolColoursGroups			= 'Cooldown Group Colours'
+L.CoolColoursGroupsDesc		= 'Set the colour for cooldowns belonging to this group. This sets the colour of the timer icon\'s border as well as the offset spoke if visible.'


 -- ------------------------
@@ -252,11 +325,18 @@ L.NotifyAlertAudio			= 'Sound'
 L.NotifyAlertAudioDesc		= 'Play a sound when this alert occurs.\n\nA setting of None can be chosen to disable audio for this alert.'
 L.NotifyAlertText			= 'Message'
 L.NotifyAlertTextDesc		= 'Display a message when this alert occurs. To choose where the message appears, use the output options below.\n\nAll alert messages are displayed in the same output location.'
-L.NotifyBrokenAuras			= 'On Broken Auras'
+L.NotifyBrokenAuras			= 'Broken Auras'
 L.NotifyBrokenAurasDesc		= 'Enable alerts for when your auras break earlier than they are intended to.\n\nNo alerts will happen if auras break due to their parent unit dying.'
-L.NotifyExpiredAuras		= 'On Expired Auras'
+L.NotifyExpiredAuras		= 'Expired Auras'
 L.NotifyExpiredAurasDesc	= 'Enable alerts for when your auras expire naturally.\n\nNo alerts will happen if auras expire due to their parent unit dying.'
-L.NotifyAlertOutputHeader	= '|cffffd100Output Alert Messages To...|r'
+L.NotifyPrematureCool		= 'Premature Cooldowns'
+L.NotifyPrematureCoolDesc	= 'Enable alerts for when your cooldowns complete earlier than expected.\n\nThis is usually caused by an ability cooldown being reset from a proc.'
+L.NotifyCompleteCool		= 'Complete Cooldowns'
+L.NotifyCompleteCoolDesc	= 'Enabled alerts for when your cooldowns complete as expected.'
+
+L.NotifyAlertOutput			= 'Output Alerts To'
+L.NotifyAlertOutputHeader	= 'All alert messages are output to the same location which can be chosen below. The list of available options includes the basic Blizzard choices as well as accommodating several popular AddOns such as SCT and MikSBT\n'
+L.NotifyAlertSinkHeader		= '|cffffd100Output Alert Messages To...|r'


 -- ------------------------
diff --git a/Ellipsis_Options/NotificationOptions.lua b/Ellipsis_Options/NotificationOptions.lua
index 62a57ec..5f0bacd 100644
--- a/Ellipsis_Options/NotificationOptions.lua
+++ b/Ellipsis_Options/NotificationOptions.lua
@@ -87,13 +87,75 @@ local notificationOptions = {
 					return not Ellipsis.db.profile.notify.auraExpiredAlerts
 				end,
 			},
+			coolPrematureAlerts = {
+				name = L.NotifyPrematureCool,
+				desc = L.NotifyPrematureCoolDesc,
+				type = 'toggle',
+				order = 7,
+			},
+			coolPrematureAudio = {
+				name = L.NotifyAlertAudio,
+				desc = L.NotifyAlertAudioDesc,
+				type = 'select',
+				order = 8,
+				width = 'half',
+				values = LSM:HashTable('sound'),
+				dialogControl = 'LSM30_Sound',
+				disabled = function()
+					return not Ellipsis.db.profile.notify.coolPrematureAlerts
+				end,
+			},
+			coolPrematureText = {
+				name = L.NotifyAlertText,
+				desc = L.NotifyAlertTextDesc,
+				type = 'toggle',
+				order = 9,
+				width = 'half',
+				disabled = function()
+					return not Ellipsis.db.profile.notify.coolPrematureAlerts
+				end,
+			},
+			coolCompleteAlerts = {
+				name = L.NotifyCompleteCool,
+				desc = L.NotifyCompleteCoolDesc,
+				type = 'toggle',
+				order = 10,
+			},
+			coolCompleteAudio = {
+				name = L.NotifyAlertAudio,
+				desc = L.NotifyAlertAudioDesc,
+				type = 'select',
+				order = 11,
+				width = 'half',
+				values = LSM:HashTable('sound'),
+				dialogControl = 'LSM30_Sound',
+				disabled = function()
+					return not Ellipsis.db.profile.notify.coolCompleteAlerts
+				end,
+			},
+			coolCompleteText = {
+				name = L.NotifyAlertText,
+				desc = L.NotifyAlertTextDesc,
+				type = 'toggle',
+				order = 12,
+				width = 'half',
+				disabled = function()
+					return not Ellipsis.db.profile.notify.coolCompleteAlerts
+				end,
+			},
+		}
+	},
+	groupAlertOutput = {
+		name = L.NotifyAlertOutput,
+		type = 'group',
+		order = -1,
+		args = {
 			outputAlertsHeader = {
 				name = L.NotifyAlertOutputHeader,
 				type = 'description',
-				order = 7,
+				order = 1,
 				width = 'full',
-				fontSize = 'medium',
-			}
+			},
 		}
 	},
 }
diff --git a/Ellipsis_Options/Options.lua b/Ellipsis_Options/Options.lua
index e242dea..10a5a4c 100644
--- a/Ellipsis_Options/Options.lua
+++ b/Ellipsis_Options/Options.lua
@@ -106,14 +106,14 @@ local options = {
 			set = 'UnitsSet',
 			-- INSERT > UNIT CONFIGURATION
 		},
-		--[[
 		cooldownOptions = {
 			name = L.CooldownHeader,
 			type = 'group',
 			order = 4,
+			get = 'CooldownsGet',
+			set = 'CooldownsSet',
 			-- INSERT > COOLDOWN SETTINGS
 		},
-		]]
 		notificationOptions = {
 			name = L.NotifyHeader,
 			type = 'group',
@@ -207,6 +207,22 @@ end
 ClearExampleAuras = function()
 	if (not exampleAurasActive) then return end -- no example auras spawned, do nothing

+	-- cleanse sample cooldowns if present
+	local activeTimers = Ellipsis.Cooldown.activeTimers
+
+	if (activeTimers['ITEM' .. -100001]) then
+		activeTimers['ITEM' .. -100001]:Release()
+	end
+
+	if (activeTimers['PET' .. -100002]) then
+		activeTimers['PET' .. -100002]:Release()
+	end
+
+	if (activeTimers['SPELL' .. -100003]) then
+		activeTimers['SPELL' .. -100003]:Release()
+	end
+
+
 	local activeUnits = Ellipsis.activeUnits

 	if (activeUnits['sample-harmful-1']) then
@@ -254,6 +270,35 @@ function Ellipsis:SpawnExampleAuras()
 	local aura, unit


+	if (self.db.profile.cooldowns.enabled) then -- only spawn sample cooldown timers if enabled
+		local CooldownTimer	= self.CooldownTimer
+		local activeTimers	= self.Cooldown.activeTimers
+		local timer
+
+		timer = activeTimers['ITEM' .. -100001] or false
+		if (timer) then
+			timer:Update(currentTime, 90)
+		else
+			CooldownTimer:New(currentTime, 'ITEM', -100001, L.SampleCoolItem, GetSpellTexture(11426), currentTime, 90)
+		end
+
+		timer = activeTimers['PET' .. -100002] or false
+		if (timer) then
+			timer:Update(currentTime, 12)
+		else
+			CooldownTimer:New(currentTime, 'PET', -100002, L.SampleCoolPet, GetSpellTexture(186257), currentTime, 12)
+		end
+
+		timer = activeTimers['SPELL' .. -100003] or false
+		if (timer) then
+			timer:Update(currentTime, 24)
+		else
+			CooldownTimer:New(currentTime, 'SPELL', -100003, L.SampleCoolSpell, GetSpellTexture(191427), currentTime, 24)
+		end
+
+		self.Cooldown:ApplyOptionsTimerRestrictions() -- apply cooldown timer restrictions to our samples
+	end
+
 	-- spawn harmful unit + auras (1)
 	unit = activeUnits['sample-harmful-1'] or Unit:New(currentTime, 'harmful', false, 'sample-harmful-1', L.SampleUnitHarmful, 'WARLOCK', -1)

@@ -418,14 +463,14 @@ function Ellipsis:OpenOptions()
 		options.args.general.args.groupControl3.args	= self:GetControl3Options()
 		options.args.auraConfiguration.args				= self:GetAuraConfiguration()
 		options.args.unitConfiguration.args				= self:GetUnitConfiguration()
---		options.args.cooldownOptions.args				= self:GetCooldownOptions()
+		options.args.cooldownOptions.args				= self:GetCooldownOptions()
 		options.args.notificationOptions.args			= self:GetNotificationOptions()

 		-- setup LibSink options
-		options.args.notificationOptions.args.groupAlert.args.outputAlerts			= self:GetSinkAce3OptionsDataTable()
-		options.args.notificationOptions.args.groupAlert.args.outputAlerts.name		= ''
-		options.args.notificationOptions.args.groupAlert.args.outputAlerts.order	= -1
-		options.args.notificationOptions.args.groupAlert.args.outputAlerts.inline	= true
+		options.args.notificationOptions.args.groupAlertOutput.args.groupSink			= self:GetSinkAce3OptionsDataTable()
+		options.args.notificationOptions.args.groupAlertOutput.args.groupSink.name		= L.NotifyAlertSinkHeader
+		options.args.notificationOptions.args.groupAlertOutput.args.groupSink.order		= 2
+		options.args.notificationOptions.args.groupAlertOutput.args.groupSink.inline	= true

 		-- setup profile options
 		options.args.general.args.groupProfiles			= LibStub('AceDBOptions-3.0'):GetOptionsTable(self.db)
@@ -449,7 +494,8 @@ function Ellipsis:OpenOptions()
 	LibStub('AceConfigDialog-3.0').Status.Ellipsis.status.groups.groups.general				= true
 	LibStub('AceConfigDialog-3.0').Status.Ellipsis.status.groups.groups.auraConfiguration	= true
 	LibStub('AceConfigDialog-3.0').Status.Ellipsis.status.groups.groups.unitConfiguration	= true
---	LibStub('AceConfigDialog-3.0').Status.Ellipsis.status.groups.groups.cooldownOptions		= true
+	LibStub('AceConfigDialog-3.0').Status.Ellipsis.status.groups.groups.cooldownOptions		= true
+	LibStub('AceConfigDialog-3.0').Status.Ellipsis.status.groups.groups.notificationOptions	= true

 	if (not hookedClose) then -- somewhat hacky, but lets us make sure samples are gone when options window closes
 		hooksecurefunc(LibStub('AceConfigDialog-3.0').OpenFrames['Ellipsis'], 'Hide', ClearExampleAuras)