--[[--
Clearcasting addon.
TODO Show tooltip hint on mouseover.
TODO Separate profiles for different character classes.
TODO Sort debuff by time remaining and importance instead of first any.
@script clearcasting
]]
local function debug(...)
if true == ClearcastingDebugFlag then
print('[Clearcasting]: ', ...)
end
end
local function getIndicatorArtworkSize()
return 24
end
local function getIndicatorFooterSize()
return 16
end
local function getIndicatorPadding()
return 4
end
local function findFirstFilterName(unitDesignation, filterDescriptor, eitherTargetNameOrId)
assert (unitDesignation ~= nil)
assert ('string' == type(unitDesignation))
assert (string.len(unitDesignation) >= 4)
assert (string.len(unitDesignation) <= 32)
assert (filterDescriptor ~= nil)
assert ('string' == type(filterDescriptor))
assert (string.len(filterDescriptor) >= 4)
assert (string.len(filterDescriptor) <= 64)
assert (eitherTargetNameOrId ~= nil)
if 'string' == type(eitherTargetNameOrId) then
local targetName = eitherTargetNameOrId
assert (targetName ~= nil)
assert ('string' == type(targetName))
--[[ The shortest spell name in English is "Hex" ]]--
assert (string.len(targetName) >= 2)
assert (string.len(targetName) <= 256)
elseif 'number' == type(eitherTargetNameOrId) then
local targetId = eitherTargetNameOrId
assert (targetId ~= nil)
assert (targetId > 0)
else
return nil
--[[error('illegal argument')]]--
end
local i = 0
while (i < 64) do
i = i + 1
local name, rank, pictureFile, stackQuantity, category,
duration, expirationInstance,
caster, stealableFlag, consolidateFlag, id = UnitAura(unitDesignation, i, filterDescriptor)
if not name then
break
end
if eitherTargetNameOrId == name or eitherTargetNameOrId == id then
return name, rank, pictureFile, stackQuantity, category,
duration, expirationInstance,
caster, stealableFlag, consolidateFlag, id
end
end
return nil
end
local function findFirstFilterCategory(unitDesignation, filterDescriptor, targetCategory)
assert (unitDesignation ~= nil)
assert ('string' == type(unitDesignation))
assert (string.len(unitDesignation) >= 4)
assert (string.len(unitDesignation) <= 32)
assert (filterDescriptor ~= nil)
assert ('string' == type(filterDescriptor))
assert (string.len(filterDescriptor) >= 4)
assert (string.len(filterDescriptor) <= 64)
assert (targetCategory ~= nil)
if 'string' == type(targetCategory) then
assert (string.len(targetCategory) >= 2)
assert (string.len(targetCategory) <= 64)
else
return nil
--[[error('illegal argument')]]--
end
local i = 0
while (i < 64) do
i = i + 1
local name, rank, pictureFile, stackQuantity, category,
duration, expirationInstance,
caster, stealableFlag, consolidateFlag, id = UnitAura(unitDesignation, i, filterDescriptor)
if not name then
break
end
if targetCategory == category then
return name, rank, pictureFile, stackQuantity, category,
duration, expirationInstance,
caster, stealableFlag, consolidateFlag, id
end
end
return nil
end
local function findAnyFilterName(unitDesignation, filterDescriptor, targetSet)
assert (unitDesignation ~= nil)
assert ('string' == type(unitDesignation))
assert (string.len(unitDesignation) >= 4)
assert (string.len(unitDesignation) <= 32)
assert (filterDescriptor ~= nil)
assert ('string' == type(filterDescriptor))
assert (string.len(filterDescriptor) >= 4)
assert (string.len(filterDescriptor) <= 64)
if 'table' == type(targetSet) then
assert (targetSet ~= nil)
assert (#targetSet >= 1)
else
return nil
end
local i = 0
debug('#begin findAny')
while (i < 64) do
i = i + 1
local name, rank, pictureFile, stackQuantity, category,
duration, expirationInstance,
caster, stealableFlag, consolidateFlag, id = UnitAura(unitDesignation, i, filterDescriptor)
local j = 0
while (j < #targetSet) do
j = j + 1
local eitherTargetNameOrId = targetSet[j]
debug('expected: ', eitherTargetNameOrId, ', actual: ', name)
if eitherTargetNameOrId == name or eitherTargetNameOrId == id then
debug('return ', name)
return name, rank, pictureFile, stackQuantity, category,
duration, expirationInstance,
caster, stealableFlag, consolidateFlag, id
end
end
end
debug('-end findAny')
return nil
end
local function findAnyHarmful(unitDesignation, filterDescriptor, targetCategory)
assert (unitDesignation ~= nil)
assert ('string' == type(unitDesignation))
assert (string.len(unitDesignation) >= 4)
assert (string.len(unitDesignation) <= 32)
assert (filterDescriptor ~= nil)
assert ('string' == type(filterDescriptor))
assert (string.len(filterDescriptor) >= 4)
assert (string.len(filterDescriptor) <= 64)
assert (targetCategory ~= nil)
if 'string' == type(targetCategory) then
assert (string.len(targetCategory) >= 2)
assert (string.len(targetCategory) <= 64)
if 'HARMFUL' ~= targetCategory then
return nil
end
else
return nil
--[[error('illegal argument')]]--
end
local i = 0
while (i < 64) do
i = i + 1
local name, rank, pictureFile, stackQuantity, category,
duration, expirationInstance,
caster, stealableFlag, consolidateFlag, id = UnitAura(unitDesignation, i, filterDescriptor)
if name and nil == category then
return name, rank, pictureFile, stackQuantity, category,
duration, expirationInstance,
caster, stealableFlag, consolidateFlag, id
end
end
return nil
end
local function applyBackground(f, pictureFile)
assert (f ~= nil)
f:SetBackdrop({bgFile = pictureFile,
edgeFile = "Interface\\AddOns\\clearcasting\\share\\2px_tooltip_border",
tile = false, tileSize = 24, edgeSize = 8,
insets = { left = 2, right = 2, top = 2, bottom = 2 }})
--f:SetBackdropColor(0.5, 0.5, 0.5, 0.5)
end
local function applyBorder(f, category, caster)
local r, g, b, a
if 'Magic' == category then
r = 0 / 255
g = 153 / 255
b = 255 / 255
a = 255 / 255
elseif 'Disease' == category then
r = 204 / 255
g = 255 / 255
b = 0 / 255
a = 255 / 255
elseif 'Curse' == category then
r = 255 / 255
g = 51 / 255
b = 255 / 255
a = 255 / 255
elseif 'Poison' == category then
r = 0 / 255
g = 204 / 255
b = 51 / 255
a = 255 / 255
elseif 'player' == caster then
r = 255 / 255
g = 255 / 255
b = 255 / 255
a = 255 / 255
else
r = 255 / 255
g = 51 / 255
b = 0 / 255
a = 255 / 255
end
f:SetBackdropBorderColor(r, g, b, a)
end
local function formatIndicatorText(duration, expirationInstance, stackQuantity)
local now = GetTime()
duration = duration or 0
expirationInstance = expirationInstance or now
local remainingDurationSec = expirationInstance - now
local t
if remainingDurationSec <= 0 or remainingDurationSec >= 60 or 0 == duration then
t = nil
else
t = string.format("%.0f", remainingDurationSec)
stackQuantity = stackQuantity or 0
if stackQuantity >= 2 and stackQuantity <= 9 then
t = tostring(math.ceil(stackQuantity)) .. '*' .. t
end
end
return t
end
local function applyDuration(f, duration, expirationInstance, stackQuantity)
assert (f ~= nil)
local now = GetTime()
duration = duration or 0
expirationInstance = expirationInstance or now
local t = formatIndicatorText(duration, expirationInstance, stackQuantity)
local textHandle = f.text
textHandle:SetText(t)
f:Show()
f.expirationInstance = expirationInstance
f.duration = duration
f.stackQuantity = stackQuantity
end
local function applyIndicatorUpdate(f)
applyDuration(f, f.duration, f.expirationInstance, f.stackQuantity)
end
local function indicatorUpdateProcessor(rootFrame, elapsedSecs)
assert (rootFrame ~= nil)
assert (elapsedSecs ~= nil)
assert (type(elapsedSecs) == 'number')
elapsedSecs = math.min(math.max(0, elapsedSecs), 999)
local duration = rootFrame.elapsedSecs
if duration == nil then
duration = 0.0
elseif type(duration) ~= 'number' then
duration = 0.0
end
duration = math.min(math.max(0, duration + elapsedSecs), 999)
rootFrame.elapsedSecs = duration
if duration >= 0.08 then
applyIndicatorUpdate(rootFrame)
rootFrame.elapsedSecs = 0.0
end
end
local function attemptToApply(f, name, rank, pictureFile, stackQuantity, category,
duration, expirationInstance,
caster, stealableFlag, consolidateFlag, id)
assert (f ~= nil)
if not name then
f.spellId = nil
f.spellName = nil
f:Hide()
f:SetScript('OnUpdate', nil)
return
end
assert (name ~= nil)
assert ('string' == type(name))
assert (string.len(name) >= 2)
assert (string.len(name) <= 256)
f.spellName = name
assert (id ~= nil)
assert ('number' == type(id))
assert (id >= 1)
f.spellId = math.floor(id)
applyBackground(f, pictureFile)
applyBorder(f, category, caster)
applyDuration(f, duration, expirationInstance, stackQuantity)
f:Show()
f:SetScript('OnUpdate', indicatorUpdateProcessor)
end
local function indicatorEventProcessor(f)
assert (f ~= nil)
local unitDesignation = f.unit or 'player'
assert (unitDesignation ~= nil)
assert ('string' == type(unitDesignation))
assert (string.len(unitDesignation) >= 4)
assert (string.len(unitDesignation) <= 32)
local filterDescriptor = f.filter or 'HELPFUL'
assert (filterDescriptor ~= nil)
assert ('string' == type(filterDescriptor))
assert (string.len(filterDescriptor) >= 4)
assert (string.len(filterDescriptor) <= 64)
local target = f.target
assert (target ~= nil)
local name, rank, pictureFile, stackQuantity, category,
duration, expirationInstance,
caster, stealableFlag, consolidateFlag, id
name, rank, pictureFile, stackQuantity, category,
duration, expirationInstance,
caster, stealableFlag, consolidateFlag, id = findFirstFilterName(unitDesignation, filterDescriptor, target)
if not name then
name, rank, pictureFile, stackQuantity, category,
duration, expirationInstance,
caster, stealableFlag, consolidateFlag, id = findFirstFilterCategory(unitDesignation, filterDescriptor, target)
end
if not name then
name, rank, pictureFile, stackQuantity, category,
duration, expirationInstance,
caster, stealableFlag, consolidateFlag, id = findAnyFilterName(unitDesignation, filterDescriptor, target)
end
if not name then
name, rank, pictureFile, stackQuantity, category,
duration, expirationInstance,
caster, stealableFlag, consolidateFlag, id = findAnyHarmful(unitDesignation, filterDescriptor, target)
end
attemptToApply(f, name, rank, pictureFile, stackQuantity, category,
duration, expirationInstance,
caster, stealableFlag, consolidateFlag, id)
end
local function tooltipOverlayEventProcessor(tooltipOverlay)
assert (tooltipOverlay ~= nil)
GameTooltip:SetOwner(tooltipOverlay, 'ANCHOR_BOTTOMRIGHT')
local button = tooltipOverlay:GetParent()
assert (button ~= nil)
GameTooltip:SetText('spell description could not be found')
if button.unit and button.index and button.filter then
GameTooltip:SetUnitAura(button.unit, button.index, button.filter);
elseif button.spellId then
local t = GetSpellLink(button.spellId)
GameTooltip:SetHyperlink(t)
end
end
local function createTooltipOverlay(indicator)
assert (indicator ~= nil)
local p = indicator:GetName() or 'Clearcasting'
local n = p .. 'TooltipOverlay'
local o = CreateFrame('FRAME', n, indicator)
o:SetAllPoints()
--[[ It is critical to call EnableMouse method on a tooltip overlay frame ]]--
o:EnableMouse(true)
o:SetScript('OnEnter', tooltipOverlayEventProcessor)
o:SetScript('OnLeave', function() GameTooltip:Hide(); end)
return o
end
local function createIndicator(parentFrame, target, unitDesignation, filterDescriptor)
assert (parentFrame ~= nil)
assert (target ~= nil)
local maxColumnQuantity = 4
local maxRowQuantity = 8
local p = parentFrame:GetName() or 'Clearcasting'
local siblingSet = {parentFrame:GetChildren()}
local siblingQuantity = #siblingSet or 0
assert (siblingQuantity >= 0)
local maxIndicatorQuantity = math.min(64, maxRowQuantity * maxColumnQuantity)
assert (siblingQuantity < maxIndicatorQuantity, 'too many indicators ' .. tostring(siblingQuantity))
local i = siblingQuantity + 1
local n = p .. 'SpellActivationOverlay' .. tostring(i)
local f = CreateFrame('FRAME', n, parentFrame)
local size = 24
local padding = 4
local paddedSize = size + padding
local y = math.floor(siblingQuantity / maxColumnQuantity)
local x = siblingQuantity - (maxColumnQuantity * y)
f:SetSize(size, size)
f:SetPoint('BOTTOMLEFT', x * paddedSize, y * (paddedSize + size * 2 / 3))
local t = f:CreateFontString(n .. 'Text', 'OVERLAY')
local fontObject = NumberFont_OutlineThick_Mono_Small
assert (fontObject ~= nil)
t:SetFontObject(fontObject)
t:SetPoint('TOPRIGHT', f, 'TOPRIGHT', 6, -f:GetHeight())
t:SetPoint('TOPLEFT', f, 'TOPLEFT', -6, -f:GetHeight())
t:SetPoint('BOTTOMLEFT', f, 'BOTTOMLEFT', 0, -f:GetHeight() * 2 / 3)
t:SetPoint('BOTTOMRIGHT', f, 'BOTTOMRIGHT', 0, -f:GetHeight() * 2 / 3)
t:SetText('?')
f.text = t
local a = f:CreateTexture(n .. 'Background', 'ARTWORK')
a:SetAllPoints()
f.background = a
f.unit = unitDesignation
f.filter = string.upper(filterDescriptor or 'HELPFUL')
f.target = target
f.spellId = nil
f.spellName = nil
f.tooltipOverlay = createTooltipOverlay(f)
f:SetScript('OnEvent', indicatorEventProcessor)
f:SetScript('OnUpdate', indicatorUpdateProcessor)
f:RegisterEvent('PLAYER_ENTERING_WORLD')
f:RegisterEvent('PLAYER_FOCUS_CHANGED')
f:RegisterEvent('PLAYER_TARGET_CHANGED')
f:RegisterEvent('UNIT_AURA')
return f
end
local function sectionEventProcessor(section)
assert (section ~= nil)
local t = {section:GetChildren()}
if #t < 1 then
return
end
local width = section:GetWidth()
local rowHeight = 44
local columnWidth = 28
local columnQuantitiy = math.floor(width / columnWidth)
local x = 0
local y = 0
local i = 0
local j = math.min(math.max(0, #t), 64)
while (i < j) do
i = i + 1
local f = t[i]
assert (f ~= nil)
indicatorEventProcessor(f)
if 1 == f:IsShown() then
f:SetPoint('BOTTOMLEFT', x * columnWidth, y * rowHeight)
x = x + 1
if x >= columnQuantitiy then
x = 0
y = y + 1
end
end
end
end
local function createSection(name, parent, width, height)
assert (name ~= nil)
name = strtrim(name)
assert ('string' == type(name))
assert (string.len(name) >= 4)
assert (string.len(name) <= 128)
assert (parent ~= nil)
assert (width ~= nil)
assert ('number' == type(width))
assert (width >= 12)
assert (width <= 1024)
assert (height ~= nil)
assert ('number' == type(height))
assert (height >= 12)
assert (height <= 1024)
local section = CreateFrame('FRAME', name, parent)
section:SetSize(width, height)
local b = section:CreateTexture(name .. 'Background', 'BACKGROUND')
b:SetAllPoints()
b:SetTexture(0.02, 0.02, 0.02, 0.12)
section.background = b
section:SetScript('OnEvent', sectionEventProcessor)
section:RegisterEvent('PLAYER_FOCUS_CHANGED')
section:RegisterEvent('PLAYER_TARGET_CHANGED')
section:RegisterEvent('UNIT_AURA')
section:RegisterEvent('UNIT_DEATH')
assert (section ~= nil)
return section
end
local function unpackAuraTable(auraTable)
assert(auraTable ~= nil)
assert("table" == type(auraTable))
local spellName = auraTable[1]
local spellRank = auraTable[2]
local pictureFile = auraTable[3]
local stackQuantity = auraTable[4]
local category = auraTable[5]
local durationSec = auraTable[6]
local expirationInstance = auraTable[7]
local casterUnitDesignation = auraTable[8]
local stealableFlag = auraTable[9]
local consolidationFlag = auraTable[10]
local spellId = auraTable[11]
return spellName, spellRank, pictureFile, stackQuantity, category, durationSec, expirationInstance,
casterUnitDesignation, stealableFlag, consolidationFlag, spellId
end
local function getAuraWeight(
eitherAuraTableOrSpellName,
spellRank,
pictureFile,
stackQuantity,
category,
durationSec,
expirationInstance,
casterUnitDesignation,
stealableFlag,
consolidationFlag,
spellId)
local spellName
if "table" == type(eitherAuraTableOrSpellName) then
local auraTable = eitherAuraTableOrSpellName
spellName,
spellRank,
pictureFile,
stackQuantity,
category,
durationSec,
expirationInstance,
casterUnitDesignation,
stealableFlag,
consolidationFlag,
spellId = unpackAuraTable(auraTable)
elseif "string" == type(eitherAuraTableOrSpellName) then
spellName = eitherAuraTableOrSpellName
else
error("invalid argument")
end
durationSec = durationSec or 1
durationSec = math.max(1, durationSec)
local now = GetTime()
expirationInstance = expirationInstance or now
local weight = 0
if "player" == casterUnitDesignation then
weight = weight + 10000
end
local categoryMap = {["Magic"] = 4000, ["Poison"] = 3000, ["Disease"] = 2000, ["Curse"] = 1000}
local categoryWeight = 0
if category then
categoryWeight = categoryMap[category] or 0
end
weight = weight + categoryWeight
--[[ FIXME Sorting by remained duraiton does not work for all spells for some reason ]]--
local durationRemainingSec = expirationInstance - now
local durationElapsedSec = durationSec - durationRemainingSec
local durationWeight = durationElapsedSec - durationSec
weight = weight + math.ceil(durationWeight)
return weight
end
local function sortUnitAuraTable(a, b)
return getAuraWeight(a) > getAuraWeight(b)
end
local function requestUnitAuraTable(unitDesignation, filterDescriptor)
assert (unitDesignation ~= nil)
assert ('string' == type(unitDesignation))
unitDesignation = strtrim(unitDesignation)
assert (string.len(unitDesignation) >= 4)
assert (string.len(unitDesignation) <= 64)
assert (filterDescriptor ~= nil)
assert ('string' == type(filterDescriptor))
filterDescriptor = strtrim(filterDescriptor)
assert (string.len(filterDescriptor) >= 4)
assert (string.len(filterDescriptor) <= 128)
local j = 0
local q = 0
local e = {}
while (j < 144) do
j = j + 1
local name, rank, pictureFile, stackQuantity, category,
duration, expirationInstance,
caster, stealableFlag, consolidateFlag, id = UnitAura(unitDesignation, j, filterDescriptor)
if name then
local auraTable = {name, rank, pictureFile, stackQuantity, category,
duration, expirationInstance,
caster, stealableFlag, consolidateFlag, id, j}
q = q + 1
e[q] = auraTable
else
break
end
end
table.sort(e, sortUnitAuraTable)
return e
end
local function subsetEventProcessor(subsetFrame)
assert (subsetFrame ~= nil)
local unitDesignation = subsetFrame.unit or 'player'
assert (unitDesignation ~= nil)
assert ('string' == type(unitDesignation))
unitDesignation = strtrim(unitDesignation)
assert (string.len(unitDesignation) >= 4)
assert (string.len(unitDesignation) <= 64)
local filterDescriptor = subsetFrame.filter or 'HELPFUL'
assert (filterDescriptor ~= nil)
assert ('string' == type(filterDescriptor))
filterDescriptor = strtrim(filterDescriptor)
assert (string.len(filterDescriptor) >= 4)
assert (string.len(filterDescriptor) <= 128)
local e = requestUnitAuraTable(unitDesignation, filterDescriptor)
local i = 0
local t = {subsetFrame:GetChildren()}
while (i < math.min(#e, #t)) do
i = i + 1
local b = t[i]
assert (b ~= nil)
local auraTable = e[i]
assert (auraTable ~= nil)
local pictureFile = auraTable[3]
assert (pictureFile ~= nil)
local artwork = b.artwork
assert (artwork ~= nil, b:GetName() .. ' requires artwork field')
artwork:SetTexture(pictureFile)
local stackQuantity = auraTable[4]
local durationSec = auraTable[6]
local expirationInstance = auraTable[7]
applyDuration(b, durationSec, expirationInstance, stackQuantity)
local spellName = auraTable[1]
assert (spellName ~= nil)
b.spell = spellName
local index = auraTable[12]
assert (index ~= nil)
b.index = index
b.filter = filterDescriptor
b.unit = unitDesignation
b:Show()
end
local k = #e
while (k < #t) do
k = k + 1
local b = t[k]
assert (b ~= nil)
local artwork = b.artwork
assert (artwork ~= nil, b:GetName() .. ' requires artwork field')
artwork:SetTexture("Interface\\Icons\\spell_nature_wispsplode")
b.spell = nil
b.index = nil
b.filter = filterDescriptor
b.unit = unitDesignation
b:Hide()
end
end
local function subsetButtonUpdateProcessor(subsetButton)
local unit = subsetButton.unit
if not unit then
return
end
local spell = subsetButton.spell
if not spell then
return
end
local filter = subsetButton.filter
local n, _, _, stackQuantity, _, durationSec, expirationInstance = UnitAura(unit, spell, nil, filter)
if not n then
return
end
applyDuration(subsetButton, durationSec, expirationInstance, stackQuantity)
end
local function createSubsetButtonArtwork(subsetButton)
assert (subsetButton ~= nil)
local buttonWidth = subsetButton:GetWidth()
local buttonHeight = subsetButton:GetHeight()
local marginBottom = math.max(buttonWidth, buttonHeight) - math.min(buttonWidth, buttonHeight)
local artwork = subsetButton:CreateTexture(subsetButton:GetName() .. 'Artwork', 'ARTWORK')
artwork:SetPoint('TOPLEFT', 0, 0)
artwork:SetPoint('TOPRIGHT', 0, 0)
artwork:SetPoint('BOTTOMLEFT', 0, marginBottom)
artwork:SetPoint('BOTTOMRIGHT', 0, marginBottom)
artwork:SetTexture("Interface\\Icons\\spell_nature_wispsplode")
return artwork
end
local function createSubsetButtonText(subsetButton)
assert (subsetButton ~= nil)
local buttonHeight = subsetButton:GetHeight()
local t = subsetButton:CreateFontString(subsetButton:GetName() .. 'Text', 'OVERLAY')
local fontObject = NumberFont_OutlineThick_Mono_Small
assert (fontObject ~= nil)
t:SetFontObject(fontObject)
t:SetPoint('BOTTOMLEFT', -4, 0)
t:SetPoint('BOTTOMRIGHT', 4, 0)
t:SetPoint('TOPRIGHT', 4, buttonHeight / -2)
t:SetPoint('TOPLEFT', -4, buttonHeight / -2)
t:SetText('?')
return t
end
local function createSubsetButton(subsetFrame, buttonDesignation, unitDesignation, spellName, filterDescriptor,
buttonWidth, buttonHeight)
assert (buttonDesignation ~= nil)
assert ('string' == type(buttonDesignation))
buttonDesignation = strtrim(buttonDesignation)
assert (string.len(buttonDesignation) >= 4)
assert (string.len(buttonDesignation) <= 256)
assert (subsetFrame ~= nil)
assert (unitDesignation ~= nil)
assert ('string' == type(unitDesignation))
unitDesignation = strtrim(unitDesignation)
assert (string.len(unitDesignation) >= 4)
assert (string.len(unitDesignation) <= 64)
if spellName then
assert ('string' == type(spellName))
spellName = strtrim(spellName)
assert (string.len(spellName) >= 2)
assert (string.len(spellName) <= 256)
end
if filterDescriptor then
assert ('string' == type(filterDescriptor))
filterDescriptor = strtrim(filterDescriptor)
assert (string.len(filterDescriptor) >= 4)
assert (string.len(filterDescriptor) <= 128)
end
if not buttonWidth then
buttonWidth = 24
end
buttonWidth = math.ceil(buttonWidth)
assert (buttonWidth ~= nil)
assert ('number' == type(buttonWidth))
assert (buttonWidth >= 8)
assert (buttonWidth <= 40)
if not buttonHeight then
buttonHeight = buttonWidth + buttonWidth * 2 / 3
end
buttonHeight = math.ceil(buttonHeight)
assert (buttonHeight ~= nil)
assert ('number' == type(buttonHeight))
assert (buttonHeight >= 8)
assert (buttonHeight <= 40)
assert (buttonWidth <= buttonHeight)
local b = CreateFrame('FRAME', buttonDesignation, subsetFrame)
b:SetSize(buttonWidth, buttonHeight)
b.artwork = createSubsetButtonArtwork(b)
b.text = createSubsetButtonText(b)
b.unit = unitDesignation
b.spell = spellName or nil
b.filter = filterDescriptor or nil
b.index = nil
--[[ TODO Add aura category (magic, disease, physical etc) border color indicator ]]--
b.tooltipOverlay = createTooltipOverlay(b)
b:SetScript('OnUpdate', subsetButtonUpdateProcessor)
return b
end
local function createSubset(parentFrame, frameDesignation, unitDesignation, filterDescriptor,
columnQuantity, rowQuantity,
buttonWidth, buttonHeight)
assert (frameDesignation ~= nil)
assert ('string' == type(frameDesignation))
frameDesignation = strtrim(frameDesignation)
assert (string.len(frameDesignation) >= 4)
assert (string.len(frameDesignation) <= 256)
assert (parentFrame ~= nil)
assert (unitDesignation ~= nil)
assert ('string' == type(unitDesignation))
unitDesignation = strtrim(unitDesignation)
assert (string.len(unitDesignation) >= 4)
assert (string.len(unitDesignation) <= 64)
assert (filterDescriptor ~= nil)
assert ('string' == type(filterDescriptor))
filterDescriptor = strtrim(filterDescriptor)
assert (string.len(filterDescriptor) >= 4)
assert (string.len(filterDescriptor) <= 128)
if not buttonWidth then
buttonWidth = 24
end
buttonWidth = math.ceil(buttonWidth)
assert (buttonWidth ~= nil)
assert ('number' == type(buttonWidth))
assert (buttonWidth >= 8)
assert (buttonWidth <= 40)
if not buttonHeight then
buttonHeight = buttonWidth + buttonWidth * 2 / 3
end
buttonHeight = math.ceil(buttonHeight)
assert (buttonHeight ~= nil)
assert ('number' == type(buttonHeight))
assert (buttonHeight >= 8)
assert (buttonHeight <= 40)
if not columnQuantity then
columnQuantity = 1
end
columnQuantity = math.min(math.max(1, math.ceil(columnQuantity)), 12)
assert (columnQuantity ~= nil)
assert ('number' == type(columnQuantity))
assert (columnQuantity >= 1)
assert (columnQuantity <= 12)
if not rowQuantity then
rowQuantity = 1
end
rowQuantity = math.min(math.max(1, math.ceil(rowQuantity)), 12)
assert (rowQuantity ~= nil)
assert ('number' == type(rowQuantity))
assert (rowQuantity >= 1)
assert (rowQuantity <= 12)
local padding = math.ceil(math.min(buttonWidth, buttonHeight) / 10)
local subsetFrame = CreateFrame('FRAME', frameDesignation, parentFrame)
subsetFrame:SetSize(buttonWidth * columnQuantity + padding * (columnQuantity + 1),
buttonHeight * rowQuantity + padding * (rowQuantity + 1))
local nameFormat = subsetFrame:GetName() .. 'Button%03d'
local k = 0
local i = 0
while (i < columnQuantity) do
i = i + 1
local j = 0
while (j < rowQuantity) do
j = j + 1
k = k + 1
local n = string.format(nameFormat, k)
local emptySpellName = nil
local b = createSubsetButton(subsetFrame, n, unitDesignation, emptySpellName, filterDescriptor,
buttonWidth, buttonHeight)
b:SetPoint('BOTTOMLEFT', buttonWidth * (i - 1) + padding * i, buttonHeight * (j - 1) + padding * j)
end
end
subsetFrame.unit = unitDesignation
subsetFrame.filter = filterDescriptor
subsetFrame:RegisterEvent('PARTY_CONVERTED_TO_RAID')
subsetFrame:RegisterEvent('PARTY_MEMBERS_CHANGED')
subsetFrame:RegisterEvent('PLAYER_FOCUS_CHANGED')
subsetFrame:RegisterEvent('PLAYER_LOGIN')
subsetFrame:RegisterEvent('PLAYER_TARGET_CHANGED')
subsetFrame:RegisterEvent('PLAYER_SPECIALIZATION_CHANGED')
subsetFrame:RegisterEvent('RAID_ROSTER_UPDATE')
subsetFrame:RegisterEvent('UNIT_AURA')
subsetFrame:RegisterEvent('UNIT_HEALTH')
subsetFrame:RegisterEvent('UPDATE_BATTLEFIELD_SCORE')
subsetFrame:SetScript('OnEvent', subsetEventProcessor)
return subsetFrame
end
local function initSpellActivationOverlayAny(rootFrame)
local margin = rootFrame:GetWidth() / 2 - 28 * 5 / 2
local d0 = createIndicator(rootFrame, 'Magic', 'player', 'HARMFUL')
d0:SetPoint('BOTTOMLEFT', margin + 28 * 0, 64)
local d1 = createIndicator(rootFrame, 'Poison', 'player', 'HARMFUL')
d1:SetPoint('BOTTOMLEFT', margin + 28 * 1, 64)
local d2 = createIndicator(rootFrame, 'Disease', 'player', 'HARMFUL')
d2:SetPoint('BOTTOMLEFT', margin + 28 * 2, 64)
local d3 = createIndicator(rootFrame, 'Curse', 'player', 'HARMFUL')
d3:SetPoint('BOTTOMLEFT', margin + 28 * 3, 64)
local d4 = createIndicator(rootFrame, 'HARMFUL', 'player', 'HARMFUL')
d4:SetPoint('BOTTOMLEFT', margin + 28 * 4, 64)
local subset0 = createSubset(rootFrame, 'ClearcastingPlayerSubset', 'player', 'PLAYER HELPFUL',
6, 1)
subset0:SetPoint('CENTER', 144 * 2, 144 * 2)
local subset1 = createSubset(rootFrame, 'ClearcastingPlayerSubset', 'target', 'PLAYER HELPFUL',
6, 1)
subset1:SetPoint('CENTER', -144 * 2, 144 * 2)
local subset2 = createSubset(rootFrame, 'ClearcastingPlayerSubset', 'target', 'HARMFUL',
6, 1)
subset2:SetPoint('CENTER', 0, 144 * 2)
end
local function initSpellActivationOverlayDeathKnight(rootFrame)
local _, classDesignation = UnitClass('player')
if 'DEATHKNIGHT' ~= classDesignation then
return
end
local sectionWidth = 288
local sectionHeight = 36
local s0 = createSection('ClearcastingDeathKnightFrame1', rootFrame, sectionWidth, sectionHeight)
s0:SetPoint('BOTTOMLEFT', rootFrame:GetWidth() / 2 - sectionWidth / 2, 144)
createIndicator(s0, 'Icebound Fortitude')
createIndicator(s0, 'Anti-Magic Shell')
createIndicator(s0, 'Anti-Magic Zone')
createIndicator(s0, 'Vampiric Blood')
createIndicator(s0, 'Icy Talons')
createIndicator(s0, 'Killing Machine')
createIndicator(s0, 'Freezing Fog')
createIndicator(s0, 'Blade Barrier')
createIndicator(s0, 'Horn of Winter')
createIndicator(s0, 'Lichborne')
local s1 = createSection('ClearcastingDeathKnightFrame2', rootFrame, sectionWidth, sectionHeight)
s1:SetPoint('BOTTOMLEFT', rootFrame:GetWidth() / 2 - sectionWidth / 2, 144 * 4)
createIndicator(s1, 'Unholy Blight', 'target', 'PLAYER HARMFUL')
createIndicator(s1, 'Frost Fever', 'target', 'PLAYER HARMFUL')
createIndicator(s1, 'Blood Plague', 'target', 'PLAYER HARMFUL')
createIndicator(s1, 'Heart Strike', 'target', 'PLAYER HARMFUL')
createIndicator(s1, 'Chains of Ice', 'target', 'PLAYER HARMFUL')
createIndicator(s1, 'Unholy Blight', 'target', 'PLAYER HARMFUL')
createIndicator(s1, 'Icy Clutch', 'target', 'PLAYER HARMFUL')
createIndicator(s1, 'Mark of Blood', 'target', 'PLAYER HARMFUL')
createIndicator(s1, 'Strangulate', 'target', 'PLAYER HARMFUL')
createIndicator(s1, 'Gnaw', 'target', 'PLAYER HARMFUL')
createIndicator(s1, 'Death Grip', 'target', 'PLAYER HARMFUL')
createIndicator(s1, 'Dark Command', 'target', 'PLAYER HARMFUL')
--[[createIndicator(s0, 'Hysteria')]]--
end
local function initSpellActivationOverlayPaladin(rootFrame)
local _, classDesignation = UnitClass('player')
if 'PALADIN' ~= classDesignation then
return
end
--[[ row 1 ]]--
local presenceSet = {
'Concentration Aura',
'Crusader Aura',
'Devotion Aura',
'Fire Resistance Aura',
'Frost Resistance Aura',
'Retribution Aura',
'Shadow Resistance Aura',
}
createIndicator(rootFrame, presenceSet, 'player', 'PLAYER HELPFUL')
local sealSet = {
'Seal of Command',
'Seal of Corruption',
'Seal of Light',
'Seal of Righteousness',
'Seal of Vengeance',
'Seal of Wisdom',
}
createIndicator(rootFrame, sealSet, 'player', 'PLAYER HELPFUL')
local blessingSet = {
'Blessing of Kings',
'Blessing of Might',
'Blessing of Sanctuary',
'Blessing of Wisdom',
'Greater Blessing of Kings',
'Greater Blessing of Might',
'Greater Blessing of Sanctuary',
'Greater Blessing of Wisdom',
}
createIndicator(rootFrame, blessingSet, 'player', 'PLAYER HELPFUL')
createIndicator(rootFrame, 'Righteous Fury')
--[[ row 2 ]]--
createIndicator(rootFrame, 'Divine Shield')
createIndicator(rootFrame, 'Divine Protection')
createIndicator(rootFrame, 'Hand of Protection')
createIndicator(rootFrame, 'Avenging Wrath')
--[[ row 3 ]]--
createIndicator(rootFrame, 'Divine Favor')
createIndicator(rootFrame, 'Divine Plea')
createIndicator(rootFrame, 'Divine Illumination')
createIndicator(rootFrame, 'Hand of Sacrifice')
--[[ row 4 ]]--
createIndicator(rootFrame, 'Judgements of the Pure')
createIndicator(rootFrame, 'Light\'s Grace')
createIndicator(rootFrame, 'Infusion of Light')
createIndicator(rootFrame, 'Holy Shield')
--[[ row 5 ]]--
createIndicator(rootFrame, 53601, 'player', 'PLAYER HELPFUL')
createIndicator(rootFrame, 58597, 'player', 'PLAYER HELPFUL')
createIndicator(rootFrame, 'Flash of Light', 'player', 'PLAYER HELPFUL')
createIndicator(rootFrame, 58597, 'focus', 'HELPFUL')
end
local function initSpellActivationOverlayWarlock(rootFrame)
assert (rootFrame ~= nil)
local _, classDesignation = UnitClass('player')
if 'WARLOCK' ~= classDesignation then
return
end
local sectionWidth = 128
local sectionHeight = 64
local wf = createSection('ClearcastingWarlockFrame', rootFrame, sectionWidth, sectionHeight)
wf:SetPoint('BOTTOMLEFT', rootFrame:GetWidth() / 2 - sectionWidth / 2, 0)
createIndicator(wf, 'Blood Fury')
createIndicator(wf, 'Sacrifice')
createIndicator(wf, 'Backlash')
createIndicator(wf, 'Decimation')
createIndicator(wf, 'Eradication')
createIndicator(wf, 'Shadow Trance')
createIndicator(wf, 'Nether Protection')
createIndicator(wf, 'Shadow Ward')
--[[ Cumulative effects. Show effects that belong to user only. ]]--
local t = {
'Drain Soul',
'Drain Life',
'Corruption',
'Haunt',
'Immolate',
'Seed of Corruption',
'Unstable Affliction',
'Shadow Embrace',
}
--[[ Conflicting or overlapping effects. Show effects that belong to any player. ]]--
local u = {
--[[ Curse ]]--
'Curse of Agony',
'Curse of Exhaustion',
'Curse of Weakness',
'Curse of the Elements',
'Curse of Tongues',
'Hex',
--[[ Magic ]]--
'Shadow Mastery',
'Entangling Roots',
'Fear',
'Polymorph',
'Psychic Scream',
'Spell Lock',
'Counterspell',
'Death Coil',
'Howl of Terror',
'Psychic Scream',
--[[ Other ]]--
'Concussion Blow',
'Cyclone',
}
local sft = createSection('ClearcastingWarlockFocusPlayerHarmful', rootFrame, sectionWidth, sectionHeight)
local sfu = createSection('ClearcastingWarlockFocusHarmful', rootFrame, sectionWidth, sectionHeight)
local spt = createSection('ClearcastingWarlockPetTargetPlayerHarmful', rootFrame, sectionWidth, sectionHeight)
local spu = createSection('ClearcastingWarlockPetTargetHarmful', rootFrame, sectionWidth, sectionHeight)
local stt = createSection('ClearcastingWarlockTargetPlayerHarmful', rootFrame, sectionWidth, sectionHeight)
local stu = createSection('ClearcastingWarlockTargetHarmful', rootFrame, sectionWidth, sectionHeight)
local marginBottom = rootFrame:GetHeight() * 2 / 3
local marginLeft = rootFrame:GetWidth()
sft:SetPoint('BOTTOMLEFT', marginLeft - sectionWidth * 2, marginBottom + sectionHeight * 0)
sfu:SetPoint('BOTTOMLEFT', marginLeft - sectionWidth * 2, marginBottom + sectionHeight * 1)
spt:SetPoint('BOTTOMLEFT', marginLeft - sectionWidth * 1, marginBottom + sectionHeight * 0)
spu:SetPoint('BOTTOMLEFT', marginLeft - sectionWidth * 1, marginBottom + sectionHeight * 1)
stt:SetPoint('BOTTOMLEFT', sectionWidth, marginBottom + sectionHeight * 0)
stu:SetPoint('BOTTOMLEFT', sectionWidth, marginBottom + sectionHeight * 1)
local p = 0
while (p < #t) do
p = p + 1
local spellName = t[p]
createIndicator(stt, spellName, 'target', 'PLAYER HARMFUL')
createIndicator(sft, spellName, 'focus', 'PLAYER HARMFUL')
createIndicator(spt, spellName, 'pettarget', 'PLAYER HARMFUL')
end
local q = 0
while (q < #u) do
q = q + 1
local spellName = u[q]
createIndicator(stu, spellName, 'target', 'HARMFUL')
createIndicator(sfu, spellName, 'focus', 'HARMFUL')
createIndicator(spu, spellName, 'pettarget', 'HARMFUL')
end
end
local function initSpellActivationOverlayWarrior(rootFrame)
local _, classDesignation = UnitClass('player')
if 'WARRIOR' ~= classDesignation then
return
end
createIndicator(rootFrame, 'Shield Wall')
createIndicator(rootFrame, 'Last Stand')
createIndicator(rootFrame, 'Enraged Regeneration')
createIndicator(rootFrame, 'Shield Block')
createIndicator(rootFrame, 'Recklessness')
createIndicator(rootFrame, 'Retaliation')
createIndicator(rootFrame, 'Berserker Rage')
createIndicator(rootFrame, 'Bloodrage')
createIndicator(rootFrame, 'Blood Fury')
createIndicator(rootFrame, 'Enrage')
createIndicator(rootFrame, 'Glyph of Blocking')
createIndicator(rootFrame, 'Sword and Board')
local x = UIParent:GetWidth() / 2 - 28 * 10 / 2
local y = 640
local f0 = createIndicator(rootFrame, 'Concussion Blow', 'target', 'PLAYER HARMFUL')
f0:SetPoint('BOTTOMLEFT', UIParent, 'BOTTOMLEFT', x + 28 * 0, y)
local f1 = createIndicator(rootFrame, 'Shockwave', 'target', 'PLAYER HARMFUL')
f1:SetPoint('BOTTOMLEFT', UIParent, 'BOTTOMLEFT', x + 28 * 1, y)
local f2 = createIndicator(rootFrame, 'Hamstring', 'target', 'PLAYER HARMFUL')
f2:SetPoint('BOTTOMLEFT', UIParent, 'BOTTOMLEFT', x + 28 * 2, y)
local f3 = createIndicator(rootFrame, 'Piercing Howl', 'target', 'PLAYER HARMFUL')
f3:SetPoint('BOTTOMLEFT', UIParent, 'BOTTOMLEFT', x + 28 * 3, y)
local f4 = createIndicator(rootFrame, 'Gag Order', 'target', 'PLAYER HARMFUL')
f4:SetPoint('BOTTOMLEFT', UIParent, 'BOTTOMLEFT', x + 28 * 4, y)
local f5 = createIndicator(rootFrame, 'Disarm', 'target', 'PLAYER HARMFUL')
f5:SetPoint('BOTTOMLEFT', UIParent, 'BOTTOMLEFT', x + 28 * 5, y)
local f6 = createIndicator(rootFrame, 'Intimidating Shout', 'target', 'PLAYER HARMFUL')
f6:SetPoint('BOTTOMLEFT', UIParent, 'BOTTOMLEFT', x + 28 * 6, y)
local f7 = createIndicator(rootFrame, 'Sunder Armor', 'target', 'PLAYER HARMFUL')
f7:SetPoint('BOTTOMLEFT', UIParent, 'BOTTOMLEFT', x + 28 * 7, y)
local f8 = createIndicator(rootFrame, 'Taunt', 'target', 'PLAYER HARMFUL')
f8:SetPoint('BOTTOMLEFT', UIParent, 'BOTTOMLEFT', x + 28 * 8, y)
local f9 = createIndicator(rootFrame, 'Mocking Blow', 'target', 'PLAYER HARMFUL')
f9:SetPoint('BOTTOMLEFT', UIParent, 'BOTTOMLEFT', x + 28 * 9, y)
local f10 = createIndicator(rootFrame, 'Challenging Shout', 'target', 'PLAYER HARMFUL')
f10:SetPoint('BOTTOMLEFT', UIParent, 'BOTTOMLEFT', x + 28 * 10, y)
end
local function initSpellActivationOverlayPriest(rootFrame)
assert (rootFrame ~= nil)
local sectionWidth = 288
local sectionHeight = 36
local s0 = createSection('ClearcastingPriestFrame1', rootFrame, sectionWidth, sectionHeight)
s0:SetPoint('BOTTOMLEFT', rootFrame:GetWidth() / 2 - sectionWidth / 2, 144)
local s1 = createSection('ClearcastingPriestFrame2', rootFrame, sectionWidth, sectionHeight)
s1:SetPoint('BOTTOMLEFT', rootFrame:GetWidth() / 2 - sectionWidth / 2, 144 * 4)
local s = {s0, s1}
local t = {'player', 'target'}
local i = 0
while (i < #t) do
i = i + 1
local section = s[i]
assert (section ~= nil)
local unitDesignation = t[i]
assert (unitDesignation ~= nil)
createIndicator(section, 'Power Word: Shield', unitDesignation, 'PLAYER HELPFUL')
createIndicator(section, 'Renew', unitDesignation, 'PLAYER HELPFUL')
createIndicator(section, 'Weakened Soul', unitDesignation, 'HARMFUL')
createIndicator(section, 'Psychic Scream', unitDesignation, 'HARMFUL')
end
createIndicator(s0, 'Surge of Light', 'player', 'PLAYER HELPFUL')
createIndicator(s0, 'Serendipity', 'player', 'PLAYER HELPFUL')
createIndicator(s0, 'Borrowed Time', 'player', 'PLAYER HELPFUL')
end
local function initSpellActivationOverlay(rootFrame)
initSpellActivationOverlayDeathKnight(rootFrame)
initSpellActivationOverlayPaladin(rootFrame)
initSpellActivationOverlayPriest(rootFrame)
initSpellActivationOverlayWarlock(rootFrame)
initSpellActivationOverlayWarrior(rootFrame)
initSpellActivationOverlayAny(rootFrame)
return {rootFrame:GetChildren()}
end
local function init(rootFrame)
rootFrame:UnregisterAllEvents()
local t = initSpellActivationOverlay(rootFrame)
assert (t ~= nil)
assert (#t >= 1)
--rootFrame:SetScript('OnEvent', eventProcessor)
rootFrame.elapsedSecs = 0.0
--rootFrame:SetScript('OnUpdate', updateProcessor)
--rootFrame:RegisterEvent('UNIT_AURA')
--rootFrame:RegisterEvent('SPELLS_CHANGED')
rootFrame.createIndicator = createIndicator
end
local function main()
assert ('enGB' == GetLocale() or 'enUS' == GetLocale())
local rootFrame = CreateFrame('FRAME', 'ClearcastingFrame', UIParent)
rootFrame:SetSize(1024, 768)
rootFrame:SetPoint('CENTER', UIParent, 'CENTER', 0, 0)
rootFrame:SetScript('OnEvent', init)
rootFrame:RegisterEvent('VARIABLES_LOADED')
end
main()