local function trace(...)
print(date('%X'), '[Choir]:', ...)
end
local function getDefaultUnitButtonBarColor()
return 0, 1, 0
end
local function createClearcastingSubset(unitButton)
assert (unitButton ~= nil)
if not ClearcastingFrame then
trace('could not access Clearcasting module')
return
end
local createSubset = ClearcastingFrame.createSubset
assert (createSubset ~= nil)
--[[ FIXME Update indicator unit designation when unit button attribute changes at runtime.
-- This is not an expected use-case. Rather it is a matter of robustness. ]]--
local unitDesignation = unitButton:GetAttribute('unit')
assert (unitDesignation ~= nil)
local harmSubset = createSubset(unitButton, unitButton:GetName() .. 'HarmfulSubsetFrame',
unitDesignation, 'HARMFUL',
5, 1)
harmSubset:SetPoint('BOTTOMLEFT', 0, 0)
--[[ TODO Track tank defensives in addition to player (healer) buffs. ]]--
local helpSubset = createSubset(unitButton, unitButton:GetName() .. 'HelpfulSubsetFrame',
unitDesignation, 'PLAYER HELPFUL',
5, 1)
helpSubset:SetPoint('BOTTOMLEFT', 0, harmSubset:GetHeight())
return harmSubset, helpSubset
end
local function createBindingKeyHandler(button)
assert (button ~= nil)
local handler = CreateFrame('FRAME', button:GetName() .. 'ChoirBindingKeyHandler',
button, 'SecureHandlerShowHideTemplate')
handler:WrapScript(button, 'OnShow', [=[
local key = self:GetAttribute('choirBindingKey')
if key then
self:SetBindingClick(true, key, self)
end
]=])
handler:WrapScript(button, 'OnHide', [=[
self:ClearBindings()
]=])
return handler
end
local function createSpellShortcut(unitButton, frameDesignation, spellName)
assert (unitButton ~= nil)
assert (spellName ~= nil)
assert ('string' == type(spellName))
spellName = strtrim(spellName)
assert (string.len(spellName) >= 2)
assert (string.len(spellName) <= 256)
local unitDesignation = unitButton:GetAttribute('unit')
assert (unitDesignation ~= nil)
assert ('string' == type(unitDesignation))
unitDesignation = strtrim(unitDesignation)
assert (string.len(unitDesignation) >= 2)
assert (string.len(unitDesignation) <= 32)
local b = CreateFrame('BUTTON', frameDesignation, unitButton, 'SecureActionButtonTemplate')
b:SetAttribute('type', 'spell')
b:SetAttribute('unit', unitDesignation)
b:SetAttribute('spell', spellName)
createBindingKeyHandler(b)
assert (b ~= nil)
return b
end
local function createLabel(ownerFrame, fontObject)
assert (ownerFrame ~= nil)
local t = ownerFrame:CreateFontString((ownerFrame:GetName() or '') .. 'Text', 'OVERLAY')
fontObject = fontObject or NumberFont_OutlineThick_Mono_Small
assert (fontObject ~= nil)
t:SetFontObject(fontObject)
t:SetAllPoints()
return t
end
local function createBackground(ownerFrame)
assert (ownerFrame ~= nil)
local background = ownerFrame:CreateTexture(ownerFrame:GetName() .. 'Background', 'BACKGROUND')
background:SetAllPoints()
background:SetTexture(0, 0, 0, 0.8)
return background
end
local function getUnitHealthDeficit(unitDesignation)
assert (unitDesignation ~= nil)
local m = UnitHealthMax(unitDesignation) or 0
local c = UnitHealth(unitDesignation) or 0
return math.abs(c) - math.abs(m)
end
local function getUnitHealthRatio(unitDesignation)
assert (unitDesignation ~= nil)
local m = math.abs(math.max(UnitHealthMax(unitDesignation) or 1, 1))
local c = math.abs(UnitHealth(unitDesignation) or 1)
return c / m
end
local function updateUnitButtonBarText(bar, unitDesignation)
assert (bar ~= nil)
local label = bar.text
assert (label ~= nil)
local t
local d = getUnitHealthDeficit(unitDesignation) or 0
if UnitIsDead(unitDesignation) then
t = '(Dead)'
elseif UnitIsGhost(unitDesignation) then
t = '(Ghost)'
elseif d < 0 then
t = tostring(math.floor(d))
else
t = nil
end
label:SetText(t)
end
local function updateUnitButtonText(self)
assert (self ~= nil)
local label = self.text
assert (label ~= nil)
local unitDesignation = self:GetAttribute('unit')
assert (unitDesignation ~= nil)
local n = UnitName(unitDesignation) or unitDesignation
local key = GetBindingKey('CLICK ' .. self:GetName() .. ':LeftButton')
if not key then
key = self:GetAttribute('choirBindingKey')
end
if key then
n = n .. ' <' .. key .. '>'
end
label:SetText(n)
end
local function getClassColor(classDesignation)
assert (classDesignation ~= nil)
assert ('string' == type(classDesignation))
classDesignation = strtrim(classDesignation)
assert (string.len(classDesignation) >= 2)
assert (string.len(classDesignation) <= 64)
local t = RAID_CLASS_COLORS[classDesignation]
if not t then
return nil
end
return t['r'], t['g'], t['b']
end
local function updateUnitButtonBarOverlay(bar, unitDesignation)
assert (bar ~= nil)
assert (unitDesignation ~= nil)
assert ('string' == type(unitDesignation))
unitDesignation = strtrim(unitDesignation)
assert (string.len(unitDesignation) >= 2)
assert (string.len(unitDesignation) <= 32)
--[[ Apply bar color update ]]--
local r, g, b = getDefaultUnitButtonBarColor()
local _, classDesignation = UnitClass(unitDesignation)
if classDesignation then
r, g, b = getClassColor(classDesignation)
end
--[[ TODO Add line of sight indicator ]]--
local a = 1
local rangeSpell = ChoirRangeSpellName
--[[ NOTE IsSpellInRange returns either 0, 1 or nil ]]--
if rangeSpell and 1 ~= IsSpellInRange(rangeSpell, unitDesignation) then
a = 1 / 2
end
local overlay = bar.overlay
assert (overlay ~= nil)
overlay:SetTexture(r, g, b, a)
--[[ Apply bar width update ]]--
local ratio = 1
if UnitExists(unitDesignation) then
ratio = getUnitHealthRatio(unitDesignation) or 1
end
ratio = math.min(math.max(0, ratio or 1), 1)
overlay:SetPoint('BOTTOMLEFT', bar, 'BOTTOMLEFT', 0, 0)
overlay:SetPoint('TOPRIGHT', bar, 'TOPRIGHT', (ratio - 1) * bar:GetWidth(), 0)
end
local function createUnitButtonBar(unitButton)
assert (unitButton ~= nil)
local n = unitButton:GetName() or ''
local padding = 4
local marginTop = 24
local marginBottom = 40 * 2 + (3 * 3)
local bar = CreateFrame('FRAME', n .. 'Bar', unitButton)
bar:SetPoint('BOTTOMLEFT', unitButton, 'BOTTOMLEFT', padding, padding + marginBottom)
bar:SetPoint('TOPRIGHT', unitButton, 'TOPRIGHT', -padding, -padding - marginTop)
local b = bar:CreateTexture(bar:GetName() .. 'Overlay', 'OVERLAY')
b:SetAllPoints()
b:SetTexture(getDefaultUnitButtonBarColor(), 1)
bar.overlay = b
local t = createLabel(bar)
bar.text = t
bar.unitButton = unitButton
--[[ Update health indicator ]]--
bar:SetScript('OnEvent', function(healthBarFrame)
assert (healthBarFrame ~= nil)
assert (unitButton ~= nil)
local u = unitButton:GetAttribute('unit')
assert (u ~= nil)
updateUnitButtonBarOverlay(healthBarFrame, u)
updateUnitButtonBarText(healthBarFrame, u)
end)
bar:RegisterEvent('PLAYER_FOCUS_CHANGED')
bar:RegisterEvent('PLAYER_TARGET_CHANGED')
bar:RegisterEvent('UNIT_HEALTH')
--[[ NOTE UNIT_SPELLCAST_* family of events are relied on to render range indicator correctly,
-- instead of the more expensive update hook. ]]--
bar:RegisterEvent('UNIT_SPELLCAST_FAILED')
bar:RegisterEvent('UNIT_SPELLCAST_FAILED_QUIET')
bar:RegisterEvent('UNIT_SPELLCAST_SENT')
bar:RegisterEvent('UNIT_SPELLCAST_START')
return bar
end
local function unitButtonEventProcessor(unitButton)
assert (unitButton ~= nil)
updateUnitButtonText(unitButton)
local bar = unitButton.bar
assert (unitButton ~= nil)
local unitDesignation = unitButton:GetAttribute('unit')
assert (unitDesignation ~= nil)
assert ('string' == type(unitDesignation))
unitDesignation = strtrim(unitDesignation)
assert (string.len(unitDesignation) >= 2)
assert (string.len(unitDesignation) <= 32)
updateUnitButtonBarOverlay(bar, unitDesignation)
updateUnitButtonBarText(bar, unitDesignation)
end
local function createInheritanceHandler(unitButton)
assert (unitButton ~= nil)
local n = (unitButton:GetName() or 'Choir') .. 'InheritanceHandler'
local inheritor = CreateFrame('FRAME', n, unitButton, 'SecureHandlerAttributeTemplate')
--[[ When a button's target unit changes, make sure that all children buttons of this button update,
-- to also target the same unit.
-- Spell shortcut feature applied by createSpellShortcut funciton depends on the inheritance handler. ]]--
inheritor:WrapScript(unitButton, 'OnAttributeChanged', [=[
local unitButton = self
local unitDesignation = unitButton:GetAttribute('unit')
if not unitDesignaiton then
return
end
local buttonChildList = unitButton:GetChildList(newtable())
local i = 0
while (i < #buttonChildList) do
i = i + 1
local child = buttonChildList[i]
child:SetAttribute('unit', unitDesignation)
end
]=])
return inheritor
end
local function createUnitButton(parentFrame, frameName, unit)
assert (parentFrame ~= nil)
assert (frameName ~= nil)
assert (unit ~= nil)
--[[ TODO Add children buttons that are secure spell buttons on the same target as the unit button.
-- Set override bindings to those buttons as ALT-A, CTRL-A, SHIFT-A, where A is the unit button key.
-- The spells assigned to the buttons let be something like Cleanse, Dispel Magic or Flash Heal.
-- That way, user may cast something without targeting or hiding the selection spoiler.
-- The problem is that it is class dependant.]]--
local u = CreateFrame('BUTTON', frameName, parentFrame, 'SecureUnitButtonTemplate')
u:SetAttribute('type', 'target')
u:SetAttribute('unit', unit)
createBindingKeyHandler(u)
createInheritanceHandler(u)
u:SetSize(24 * 5 + 3 * 6, 40 * 2 + 3 * 3 + 24 * 2)
local t = createLabel(u)
t:SetPoint('BOTTOMLEFT', u, 'BOTTOMLEFT', 4, u:GetHeight() - 24 - 4)
t:SetPoint('TOPRIGHT', u, 'TOPRIGHT', -4, -4)
u.text = t
local b = createBackground(u)
u.background = b
local bar = createUnitButtonBar(u)
u.bar = bar
createClearcastingSubset(u)
u:SetScript('OnEvent', unitButtonEventProcessor)
u:RegisterEvent('PARTY_CONVERTED_TO_RAID')
u:RegisterEvent('PARTY_MEMBERS_CHANGED')
u:RegisterEvent('PLAYER_ALIVE')
u:RegisterEvent('RAID_ROSTER_UPDATE')
u:RegisterEvent('UPDATE_BATTLEFIELD_SCORE')
u:RegisterEvent('ADDON_LOADED')
assert (u ~= nil)
return u
end
local function createSpoiler(spoilerParent, spoilerDesignation)
local spoiler = CreateFrame('BUTTON', spoilerDesignation, spoilerParent, 'SecureHandlerClickTemplate')
spoiler:EnableMouse(true)
spoiler:SetAttribute('_onclick', [=[
--[[ Toggle the spoiler on repeated clicks. ]]--
if self:IsShown() then
self:Hide()
return
end
--[[ Override binding key to toggle the spoiler on Escape key press. ]]--
self:SetBindingClick(true, 'CTRL-W', self)
self:SetBindingClick(true, 'ESCAPE', self)
self:Show()
local childTable = self:GetChildList(newtable())
local i = 0
while (i < #childTable) do
i = i + 1
local child = childTable[i]
child:Show()
end
]=])
--[[ Toggle sibling frames, which are other spoilers that contain unit buttons ]]--
--[[ WARNING Make sure not to toggle this frame (self).
-- Otherwise wrap scripts might trigger unexpectedly. ]]--
spoiler:WrapScript(spoiler, 'OnShow', [=[
local parentFrame = self:GetParent()
local siblingTable = parentFrame:GetChildList(newtable())
local i = 0
while (i < #siblingTable) do
i = i + 1
local sibling = siblingTable[i]
if self ~= sibling then
sibling:Hide()
end
end
]=])
spoiler:WrapScript(spoiler, 'OnHide', [=[
self:ClearBindings()
]=])
createBindingKeyHandler(spoiler)
return spoiler
end
local function getButtonBindingKeyExplicit(buttonRef)
assert (buttonRef ~= nil)
local key = GetBindingKey('CLICK ' .. buttonRef:GetName() .. ':LeftButton')
return key
end
local function getButtonBindingKeyDefault(buttonNumber)
assert (buttonNumber ~= nil)
assert ('number' == type(buttonNumber))
buttonNumber = math.abs(math.floor(buttonNumber))
local key
local actionBarSize = 12
if buttonNumber >= 1 and buttonNumber <= actionBarSize then
key = GetBindingKey('ACTIONBUTTON' .. tostring(buttonNumber))
elseif buttonNumber > actionBarSize and buttonNumber <= actionBarSize * 6 then
local r = buttonNumber % actionBarSize
local n = buttonNumber - r * actionBarSize
key = GetBindingKey('MULTIACTIONBAR' .. r .. 'BUTTON' .. n)
else
key = nil
end
return key
end
local function getShortcutBindingKeySuggestion(buttonNumber, spellNumber)
assert (buttonNumber ~= nil)
assert (buttonNumber >= 1 and buttonNumber <= 5)
assert (spellNumber ~= nil)
assert (spellNumber >= 1 and spellNumber <= 5)
local map = {
{'Q', 'ALT-Q', 'ALT-A', 'SHIFT-A', '1',},
{'W', 'ALT-W', 'ALT-S', 'SHIFT-S', '2',},
{'E', 'ALT-E', 'ALT-D', 'SHIFT-D', '3',},
{'R', 'ALT-R', 'ALT-F', 'SHIFT-F', '4',},
{'T', 'ALT-T', 'ALT-G', 'SHIFT-G', '5',},
}
local bindingList = map[buttonNumber]
return bindingList[spellNumber]
end
local function createUnitButtonSpellShortcut(unitButton, buttonNumber)
assert (unitButton ~= nil)
--[[ FIXME Add shortcuts for all classes and use cases ]]--
local spellSet = {
'Dispel Magic',
'Abolish Disease',
'Renew',
'Power Word: Shield',
'Flash Heal',
}
local spellNumber = 0
while (spellNumber < #spellSet) do
spellNumber = spellNumber + 1
local n = unitButton:GetName() .. 'Shortcut' .. tostring(spellNumber)
local spellName = spellSet[spellNumber]
local b = createSpellShortcut(unitButton, n, spellName)
local key = getShortcutBindingKeySuggestion(buttonNumber, spellNumber)
b:SetAttribute('choirBindingKey', key)
end
end
local function createGroup(rootFrame, groupNumber, unitTable)
assert (rootFrame ~= nil)
assert (groupNumber ~= nil)
groupNumber = math.floor(math.abs(groupNumber))
assert ((groupNumber >= 1 and groupNumber <= 8) or unitTable ~= nil)
local groupSize = 5
local u
if unitTable then
u = unitTable
else
local q = 0
u = {}
while (q < groupSize) do
q = q + 1
u[q] = 'raid' .. tostring((groupNumber - 1) * groupSize + q)
end
end
assert (u ~= nil)
assert ('table' == type(u))
assert (#u == groupSize)
local spoiler = createSpoiler(rootFrame, 'ChoirSpoiler' .. tostring(groupNumber))
local i = 0
local marginLeft = 0
local padding = 4
while (i < #u) do
i = i + 1
local unitDesignation = u[i]
assert (unitDesignation ~= nil)
local memberNumber = (groupNumber - 1) * groupSize + i
local b = createUnitButton(spoiler, 'ChoirUnitButton' .. tostring(memberNumber), unitDesignation)
b:SetPoint('BOTTOMLEFT', marginLeft, 0)
marginLeft = marginLeft + b:GetWidth() + padding
spoiler:WrapScript(b, 'OnClick', [=[
--[[ Assume that parent is the spoiler frame
-- which was created with createSpoiler and
-- has required scripts hooked to it ]]--
local spoilerFrame = self:GetParent()
spoilerFrame:Hide()
]=])
b:Hide()
local key = getButtonBindingKeyExplicit(b) or getButtonBindingKeyDefault(i)
if key then
b:SetAttribute('choirBindingKey', key)
end
_G['BINDING_NAME_CLICK ' .. b:GetName() .. ':LeftButton'] = 'Unit ' .. tostring(i)
createUnitButtonSpellShortcut(b, i)
end
spoiler:SetSize(marginLeft, 144)
spoiler:SetPoint('CENTER', 0, 12 * 6)
_G['BINDING_NAME_CLICK ' .. spoiler:GetName() .. ':LeftButton'] = 'Group ' .. tostring(groupNumber)
spoiler:Hide()
return spoiler
end
local function getRangeSpellNameSuggestion()
local _, classDesignation = UnitClass('player')
assert (classDesignation ~= nil)
assert ('string' == type(classDesignation))
classDesignation = strtrim(classDesignation)
assert (string.len(classDesignation) >= 2)
assert (string.len(classDesignation) <= 64)
local map = {
['DRUID'] = {'Cure Poison', 'Healing Touch'},
['PALADIN'] = {'Cleanse', 'Purify', 'Holy Light'},
['PRIEST'] = {'Dispel Magic', 'Cure Disease', 'Lesser Heal'},
['SHAMAN'] = {'Healing Wave'},
}
local t = map[classDesignation]
if not t then
return
end
assert (t ~= nil)
assert ('table' == type(t))
assert (#t >= 1)
local s = nil
local i = 0
while (i < #t) do
i = i + 1
local candidate = t[i]
assert (candidate ~= nil)
local spellName = GetSpellInfo(candidate)
if spellName then
s = spellName
break
end
end
return s
end
local function initRangeSpellName(rootFrame)
local f = CreateFrame('FRAME', rootFrame:GetName() .. 'RangeSpellFrame')
f:SetScript('OnEvent', function()
ChoirRangeSpellName = getRangeSpellNameSuggestion()
end)
f:RegisterEvent('SPELLS_CHANGED')
local s = ChoirRangeSpellName
if s then
assert (s ~= nil)
assert ('string' == type(s))
s = strtrim(s)
assert (string.len(s) >= 2)
assert (string.len(s) <= 256)
else
s = getRangeSpellNameSuggestion()
end
ChoirRangeSpellName = s
return s
end
local function initSpoiler(rootFrame)
assert (rootFrame ~= nil)
createGroup(rootFrame, 1)
createGroup(rootFrame, 2)
createGroup(rootFrame, 3)
createGroup(rootFrame, 4)
createGroup(rootFrame, 5)
createGroup(rootFrame, 6)
createGroup(rootFrame, 7)
createGroup(rootFrame, 8)
--[[ TODO Add Esc key that closes the spoiler without clicking any of the children buttons ]]--
--[[ TODO Generalize the interface to be used with any kind of child frames,
-- especially nested spoilers and spell buttons. ]]--
--[[ NOTE To get a saved variables kind of table from a restricted frame snippet, use ```
-- local env = GetManagedEnvironment(headerRef)
-- local t = env['MySavedVariableTable']
-- ```
-- See http://www.iriel.org/wow/docs/SecureHeadersGuide-4.0-r1.pdf ]]--
local spoilerParty = createGroup(rootFrame, 9, {'player', 'party1', 'party2', 'party3', 'party4'})
_G['BINDING_NAME_CLICK ' .. spoilerParty:GetName() .. ':LeftButton'] = 'Player party'
BINDING_HEADER_CHOIR = 'Choir'
end
local function init(rootFrame)
assert (rootFrame ~= nil)
local locale = GetLocale()
assert (locale == 'enGB' or locale == 'enUS', 'requires English localization')
rootFrame:UnregisterAllEvents()
rootFrame:SetSize(1024, 768)
rootFrame:SetPoint('CENTER', 0, 0)
initRangeSpellName(rootFrame)
initSpoiler(rootFrame)
trace('init')
end
local function main()
local rootFrame = CreateFrame('FRAME', 'ChoirFrame', UIParent)
--[[ NOTE The add-on requires key bindings data.
-- Therefore trigger the add-on initialization after the key bindings were loaded. ]]--
rootFrame:RegisterEvent('PLAYER_LOGIN')
rootFrame:SetScript('OnEvent', init)
end
main()