You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2448 lines
73 KiB

---@class Cell
local Cell = select(2, ...)
local L = Cell.L
---@class CellFuncs
local F = Cell.funcs
---@type CellIndicatorFuncs
local I = Cell.iFuncs
Cell.vars.playerFaction = UnitFactionGroup("player")
-------------------------------------------------
-- game version
-------------------------------------------------
Cell.isAsian = LOCALE_zhCN or LOCALE_zhTW or LOCALE_koKR
Cell.isRetail = WOW_PROJECT_ID == WOW_PROJECT_MAINLINE
Cell.isVanilla = WOW_PROJECT_ID == WOW_PROJECT_CLASSIC
-- Cell.isBCC = WOW_PROJECT_ID == WOW_PROJECT_BURNING_CRUSADE_CLASSIC and LE_EXPANSION_LEVEL_CURRENT == LE_EXPANSION_BURNING_CRUSADE
-- Cell.isWrath = WOW_PROJECT_ID == WOW_PROJECT_WRATH_CLASSIC and LE_EXPANSION_LEVEL_CURRENT == LE_EXPANSION_WRATH_OF_THE_LICH_KING
Cell.isWrath = WOW_PROJECT_ID == WOW_PROJECT_WRATH_CLASSIC
Cell.isCata = WOW_PROJECT_ID == WOW_PROJECT_CATACLYSM_CLASSIC
Cell.isMists = WOW_PROJECT_ID == WOW_PROJECT_MISTS_CLASSIC
Cell.isTWW = LE_EXPANSION_LEVEL_CURRENT == LE_EXPANSION_WAR_WITHIN
if Cell.isRetail then
Cell.flavor = "retail"
elseif Cell.isMists then
Cell.flavor = "mists"
elseif Cell.isCata then
Cell.flavor = "cata"
elseif Cell.isWrath then
Cell.flavor = "wrath"
elseif Cell.isVanilla then
Cell.flavor = "vanilla"
end
-------------------------------------------------
-- class
-------------------------------------------------
local localizedClass
if Cell.isRetail then
localizedClass = LocalizedClassList()
else
localizedClass = {}
FillLocalizedClassList(localizedClass)
end
local sortedClasses = {}
local classFileToID = {}
local classIDToFile = {}
do
-- WARRIOR = 1,
-- PALADIN = 2,
-- HUNTER = 3,
-- ROGUE = 4,
-- PRIEST = 5,
-- DEATHKNIGHT = 6,
-- SHAMAN = 7,
-- MAGE = 8,
-- WARLOCK = 9,
-- MONK = 10,
-- DRUID = 11,
-- DEMONHUNTER = 12,
-- EVOKER = 13,
--! GetNumClasses returns the highest class ID (NOT IN CLASSIC)
local highestClassID = GetNumClasses()
if highestClassID < 11 then highestClassID = 11 end
for i = 1, highestClassID do
local classFile, classID = select(2, GetClassInfo(i))
if classFile and classID == i then
tinsert(sortedClasses, classFile)
classFileToID[classFile] = i
classIDToFile[i] = classFile
end
end
sort(sortedClasses)
end
function F.GetClassID(classFile)
return classFileToID[classFile]
end
function F.GetLocalizedClassName(classFileOrID)
if type(classFileOrID) == "string" then
return localizedClass[classFileOrID] or classFileOrID
elseif type(classFileOrID) == "number" and classIDToFile[classFileOrID] then
return localizedClass[classIDToFile[classFileOrID]] or classFileOrID
end
return ""
end
function F.IterateClasses()
local i = 0
return function()
i = i + 1
if i <= GetNumClasses() then
return sortedClasses[i], classFileToID[sortedClasses[i]], i
end
end
end
function F.GetSortedClasses()
return F.Copy(sortedClasses)
end
-------------------------------------------------
-- Classic
-------------------------------------------------
if Cell.isCata then
function F.GetActiveTalentInfo()
local which = GetActiveTalentGroup() == 1 and L["Primary Talents"] or L["Secondary Talents"]
return which, Cell.vars.playerSpecIcon, Cell.vars.playerSpecName
end
elseif Cell.isWrath or Cell.isVanilla then
function F.GetActiveTalentInfo()
local which = GetActiveTalentGroup() == 1 and L["Primary Talents"] or L["Secondary Talents"]
local maxPoints = 0
local specName, specIcon, specFileName
for i = 1, GetNumTalentTabs() do
local id, name, description, icon, pointsSpent, background = GetTalentTabInfo(i)
if pointsSpent > maxPoints then
maxPoints = pointsSpent
specIcon = icon
specName = name
-- elseif pointsSpent == maxPoints then
-- specIcon = 132148
end
end
return which, specIcon or 134400, specName or L["No Spec"]
end
end
-- local specRoles = {
-- ["DeathKnightBlood"] = "DAMAGER",
-- ["DeathKnightFrost"] = "TANK",
-- ["DeathKnightUnholy"] = "DAMAGER",
-- ["DruidRestoration"] = "HEALER",
-- ["DruidBalance"] = "DAMAGER",
-- -- ["DruidFeralCombat"] = nil,
-- ["HunterBeastMastery"] = "DAMAGER",
-- ["HunterSurvival"] = "DAMAGER",
-- ["HunterMarksmanship"] = "DAMAGER",
-- ["MageFrost"] = "DAMAGER",
-- ["MageArcane"] = "DAMAGER",
-- ["MageFire"] = "DAMAGER",
-- ["PaladinHoly"] = "HEALER",
-- ["PaladinCombat"] = "DAMAGER",
-- ["PaladinProtection"] = "TANK",
-- ["PriestShadow"] = "DAMAGER",
-- ["PriestHoly"] = "HEALER",
-- ["PriestDiscipline"] = "HEALER",
-- ["RogueCombat"] = "DAMAGER",
-- ["RogueSubtlety"] = "DAMAGER",
-- ["RogueAssassination"] = "DAMAGER",
-- ["ShamanElementalCombat"] = "DAMAGER",
-- ["ShamanEnhancement"] = "DAMAGER",
-- ["ShamanRestoration"] = "HEALER",
-- ["WarlockSummoning"] = "DAMAGER",
-- ["WarlockDestruction"] = "DAMAGER",
-- ["WarlockCurses"] = "DAMAGER",
-- ["WarriorArms"] = "DAMAGER",
-- ["WarriorFury"] = "DAMAGER",
-- ["WarriorProtection"] = "TANK",
-- }
-- function F.GetPlayerRole()
-- end
-------------------------------------------------
-- color
-------------------------------------------------
function F.ConvertRGB(r, g, b, desaturation)
if not desaturation then desaturation = 1 end
r = r / 255 * desaturation
g = g / 255 * desaturation
b = b / 255 * desaturation
return r, g, b
end
function F.ConvertRGB_256(r, g, b)
return floor(r * 255), floor(g * 255), floor(b * 255)
end
function F.ConvertRGBToHEX(r, g, b)
local result = ""
for key, value in pairs({r, g, b}) do
local hex = ""
while(value > 0)do
local index = math.fmod(value, 16) + 1
value = math.floor(value / 16)
hex = string.sub("0123456789ABCDEF", index, index) .. hex
end
if(string.len(hex) == 0)then
hex = "00"
elseif(string.len(hex) == 1)then
hex = "0" .. hex
end
result = result .. hex
end
return result
end
function F.ConvertHEXToRGB(hex)
hex = hex:gsub("#","")
return tonumber("0x"..hex:sub(1,2)), tonumber("0x"..hex:sub(3,4)), tonumber("0x"..hex:sub(5,6))
end
-- https://wowpedia.fandom.com/wiki/ColorGradient
-- function F.ColorGradient(perc, r1,g1,b1, r2,g2,b2, r3,g3,b3)
-- perc = perc or 1
-- if perc >= 1 then
-- return r3, g3, b3
-- elseif perc <= 0 then
-- return r1, g1, b1
-- end
-- local segment, relperc = math.modf(perc * 2)
-- local rr1, rg1, rb1, rr2, rg2, rb2 = select((segment * 3) + 1, r1,g1,b1, r2,g2,b2, r3,g3,b3)
-- return rr1 + (rr2 - rr1) * relperc, rg1 + (rg2 - rg1) * relperc, rb1 + (rb2 - rb1) * relperc
-- end
function F.ColorGradient(perc, c1, c2, c3, lowBound, highBound)
local r1, g1, b1 = c1[1], c1[2], c1[3]
local r2, g2, b2 = c2[1], c2[2], c2[3]
local r3, g3, b3 = c3[1], c3[2], c3[3]
lowBound = lowBound or 0
highBound = highBound or 1
perc = perc or 1
if perc >= highBound then
return r3, g3, b3
elseif perc <= lowBound then
return r1, g1, b1
end
perc = (perc - lowBound) / (highBound - lowBound)
local segment, relperc = math.modf(perc * 2)
local rr1, rg1, rb1, rr2, rg2, rb2 = select((segment * 3) + 1, r1,g1,b1, r2,g2,b2, r3,g3,b3)
return rr1 + (rr2 - rr1) * relperc, rg1 + (rg2 - rg1) * relperc, rb1 + (rb2 - rb1) * relperc
end
function F.ColorThreshold(perc, c1, c2, c3, lowBound, highBound, useThresholdColor)
if useThresholdColor then
return F.ColorGradient(perc, c1, c2, c3, lowBound, highBound)
end
lowBound = lowBound or 0
highBound = highBound or 1
perc = perc or 1
if perc >= highBound then
return c3[1], c3[2], c3[3]
elseif perc >= lowBound then
return c2[1], c2[2], c2[3]
else
return c1[1], c1[2], c1[3]
end
end
--! From ColorPickerAdvanced by Feyawen-Llane
--[[ Convert RGB to HSV ---------------------------------------------------
Inputs:
r = Red [0, 1]
g = Green [0, 1]
b = Blue [0, 1]
Outputs:
H = Hue [0, 360]
S = Saturation [0, 1]
B = Brightness [0, 1]
]]--
function F.ConvertRGBToHSB(r, g, b)
local colorMax = max(max(r, g), b)
local colorMin = min(min(r, g), b)
local delta = colorMax - colorMin
local H, S, B
-- WoW's LUA doesn't handle floating point numbers very well (Somehow 1.000000 != 1.000000 WTF?)
-- So we do this weird conversion of, Number to String back to Number, to make the IF..THEN work correctly!
colorMax = tonumber(format("%f", colorMax))
r = tonumber(format("%f", r))
g = tonumber(format("%f", g))
b = tonumber(format("%f", b))
if (delta > 0) then
if (colorMax == r) then
H = 60 * (((g - b) / delta) % 6)
elseif (colorMax == g) then
H = 60 * (((b - r) / delta) + 2)
elseif (colorMax == b) then
H = 60 * (((r - g) / delta) + 4)
end
if (colorMax > 0) then
S = delta / colorMax
else
S = 0
end
B = colorMax
else
H = 0
S = 0
B = colorMax
end
if (H < 0) then
H = H + 360
end
return H, S, B
end
--[[ Convert HSB to RGB ---------------------------------------------------
Inputs:
h = Hue [0, 360]
s = Saturation [0, 1]
b = Brightness [0, 1]
Outputs:
R = Red [0,1]
G = Green [0,1]
B = Blue [0,1]
]]--
function F.ConvertHSBToRGB(h, s, b)
local chroma = b * s
local prime = (h / 60) % 6
local X = chroma * (1 - abs((prime % 2) - 1))
local M = b - chroma
local R, G, B
if (0 <= prime) and (prime < 1) then
R = chroma
G = X
B = 0
elseif (1 <= prime) and (prime < 2) then
R = X
G = chroma
B = 0
elseif (2 <= prime) and (prime < 3) then
R = 0
G = chroma
B = X
elseif (3 <= prime) and (prime < 4) then
R = 0
G = X
B = chroma
elseif (4 <= prime) and (prime < 5) then
R = X
G = 0
B = chroma
elseif (5 <= prime) and (prime < 6) then
R = chroma
G = 0
B = X
else
R = 0
G = 0
B = 0
end
R = R + M
G = G + M
B = B + M
return R, G, B
end
function F.InvertColor(r, g, b)
return 1 - r, 1 - g, 1 - b
end
-------------------------------------------------
-- number
-------------------------------------------------
function F.Round(num, numDecimalPlaces)
if numDecimalPlaces and numDecimalPlaces >= 0 then
local mult = 10 ^ numDecimalPlaces
num = num * mult
if num >= 0 then
return floor(num + 0.5) / mult
else
return ceil(num - 0.5) / mult
end
end
if num >= 0 then
return floor(num + 0.5)
else
return ceil(num - 0.5)
end
end
local symbol_1K, symbol_10K, symbol_1B
if LOCALE_zhCN then
symbol_1K, symbol_10K, symbol_1B = "", "", "亿"
elseif LOCALE_zhTW then
symbol_1K, symbol_10K, symbol_1B = "", "", ""
elseif LOCALE_koKR then
symbol_1K, symbol_10K, symbol_1B = "", "", ""
end
local abs = math.abs
if Cell.isAsian then
function F.FormatNumber(n)
if abs(n) >= 100000000 then
return F.Round(n / 100000000, 2) .. symbol_1B
elseif abs(n) >= 10000 then
return F.Round(n / 10000, 1) .. symbol_10K
else
return n
end
end
else
function F.FormatNumber(n)
if abs(n) >= 1000000000 then
return F.Round(n / 1000000000, 2) .. "B"
elseif abs(n) >= 1000000 then
return F.Round(n / 1000000, 2) .. "M"
elseif abs(n) >= 1000 then
return F.Round(n / 1000, 1) .. "K"
else
return n
end
end
end
-------------------------------------------------
-- string
-------------------------------------------------
function F.UpperFirst(str, lowerOthers)
if lowerOthers then
str = strlower(str)
end
return (str:gsub("^%l", string.upper))
end
function F.SplitToNumber(sep, str)
if not str then return end
local ret = {strsplit(sep, str)}
for i, v in ipairs(ret) do
ret[i] = tonumber(v) or ret[i] -- keep non number
end
return unpack(ret)
end
local function Chsize(char)
if not char then
return 0
elseif char > 240 then
return 4
elseif char > 225 then
return 3
elseif char > 192 then
return 2
else
return 1
end
end
function F.Utf8sub(str, startChar, numChars)
if not str then return "" end
local startIndex = 1
while startChar > 1 do
local char = string.byte(str, startIndex)
startIndex = startIndex + Chsize(char)
startChar = startChar - 1
end
local currentIndex = startIndex
while numChars > 0 and currentIndex <= #str do
local char = string.byte(str, currentIndex)
currentIndex = currentIndex + Chsize(char)
numChars = numChars -1
end
return str:sub(startIndex, currentIndex - 1)
end
function F.FitWidth(fs, text, alignment)
fs:SetText(text)
if fs:IsTruncated() then
for i = 1, string.utf8len(text) do
if strlower(alignment) == "right" then
fs:SetText("..."..string.utf8sub(text, i))
else
fs:SetText(string.utf8sub(text, i).."...")
end
if not fs:IsTruncated() then
break
end
end
end
end
-------------------------------------------------
-- table
-------------------------------------------------
function F.Getn(t)
local count = 0
for k, v in pairs(t) do
count = count + 1
end
return count
end
function F.GetIndex(t, e)
for i, v in pairs(t) do
if e == v then
return i
end
end
return nil
end
function F.GetKeys(t)
local keys = {}
for k in pairs(t) do
tinsert(keys, k)
end
return keys
end
function F.Copy(t)
local newTbl = {}
for k, v in pairs(t) do
if type(v) == "table" then
newTbl[k] = F.Copy(v)
else
newTbl[k] = v
end
end
return newTbl
end
function F.TContains(t, v)
for _, value in pairs(t) do
if value == v then return true end
end
return false
end
function F.TInsert(t, v)
local i, done = 1
repeat
if not t[i] then
t[i] = v
done = true
end
i = i + 1
until done
end
function F.TInsertIfNotExists(t, ...)
local n = select("#", ...)
if n == 0 then return end
if n == 1 then
local v = ...
if not F.TContains(t, v) then
tinsert(t, v)
end
else
local values = F.ConvertTable(t, true)
for i = 1, n do
local v = select(i, ...)
if not values[v] then
tinsert(t, v)
end
end
values = nil
end
end
function F.TRemove(t, v)
for i = #t, 1, -1 do
if t[i] == v then
table.remove(t, i)
end
end
end
function F.TMergeOverwrite(...)
local n = select("#", ...)
if n == 0 then return {} end
local temp = F.Copy(...)
for i = 2, n do
local t = select(i, ...)
for k, v in pairs(t) do
temp[k] = v
end
end
return temp
end
function F.RemoveElementsExceptKeys(tbl, ...)
local keys = {}
for i = 1, select("#", ...) do
local k = select(i, ...)
keys[k] = true
end
for k in pairs(tbl) do
if not keys[k] then
tbl[k] = nil
end
end
end
function F.RemoveElementsByKeys(tbl, ...)
for i = 1, select("#", ...) do
local k = select(i, ...)
tbl[k] = nil
end
end
function F.Sort(t, k1, order1, k2, order2, k3, order3)
table.sort(t, function(a, b)
if a[k1] ~= b[k1] then
if order1 == "ascending" then
return a[k1] < b[k1]
else -- "descending"
return a[k1] > b[k1]
end
elseif k2 and order2 and a[k2] ~= b[k2] then
if order2 == "ascending" then
return a[k2] < b[k2]
else -- "descending"
return a[k2] > b[k2]
end
elseif k3 and order3 and a[k3] ~= b[k3] then
if order3 == "ascending" then
return a[k3] < b[k3]
else -- "descending"
return a[k3] > b[k3]
end
end
end)
end
function F.StringToTable(s, sep, convertToNum)
local t = {}
for i, v in pairs({string.split(sep, s)}) do
v = strtrim(v)
if v ~= "" then
if convertToNum then
v = tonumber(v)
if v then tinsert(t, v) end
else
tinsert(t, v)
end
end
end
return t
end
function F.TableToString(t, sep)
return table.concat(t, sep)
end
function F.ConvertTable(t, value)
local temp = {}
for k, v in ipairs(t) do
temp[v] = value or k
end
return temp
end
function F.ConvertSpellTable(t, convertIdToName)
if not convertIdToName then
return F.ConvertTable(t)
end
local temp = {}
for k, v in ipairs(t) do
local name = F.GetSpellInfo(v)
if name then
temp[name] = k
end
end
return temp
end
function F.ConvertSpellTable_WithColor(t, convertIdToName)
local temp = {}
for k, st in ipairs(t) do
local index
if convertIdToName then
index = F.GetSpellInfo(st[1])
else
index = st[1]
end
if index then
temp[index] = {k, st[2]}
end
end
return temp
end
function F.ConvertSpellTable_WithClass(t)
local temp = {}
for class, ct in pairs(t) do
for _, id in ipairs(ct) do
local name = F.GetSpellInfo(id)
if name then
temp[id] = true
end
end
end
return temp
end
function F.ConvertSpellDurationTable(t, convertIdToName)
local temp = {}
for _, v in ipairs(t) do
local id, duration = strsplit(":", v)
local name = F.GetSpellInfo(id)
if name then
if convertIdToName then
temp[name] = tonumber(duration)
else
temp[tonumber(id)] = tonumber(duration)
end
end
end
return temp
end
function F.ConvertSpellDurationTable_WithClass(t)
local temp = {}
for class, ct in pairs(t) do
for k, v in ipairs(ct) do
local id, duration = strsplit(":", v)
local name, icon = F.GetSpellInfo(id)
if name then
temp[tonumber(id)] = {tonumber(duration), icon}
end
end
end
return temp
end
function F.CheckTableRemoved(previous, after)
local aa = {}
local ret = {}
for k,v in pairs(previous) do aa[v] = true end
for k,v in pairs(after) do aa[v] = nil end
for k,v in pairs(previous) do
if aa[v] then
tinsert(ret, v)
end
end
return ret
end
function F.FilterInvalidSpells(t)
if not t then return end
for i = #t, 1, -1 do
local spellId
if type(t[i]) == "number" then
spellId = t[i]
else -- table
spellId = t[i][1]
end
if not F.GetSpellInfo(spellId) then
tremove(t, i)
end
end
end
-------------------------------------------------
-- general
-------------------------------------------------
-- function F.GetRealmName()
-- return string.gsub(GetRealmName(), " ", "")
-- end
function F.UnitFullName(unit)
if not unit or not UnitIsPlayer(unit) then return end
local name = GetUnitName(unit, true)
--? name might be nil in some cases?
if name and not string.find(name, "-") then
local server = GetNormalizedRealmName()
--? server might be nil in some cases?
if server then
name = name.."-"..server
end
end
return name
end
function F.ToShortName(fullName)
if not fullName then return "" end
local shortName = strsplit("-", fullName)
return shortName
end
function F.FormatTime(s)
if s >= 3600 then
return "%dh", ceil(s / 3600)
elseif s >= 60 then
return "%dm", ceil(s / 60)
end
return "%ds", floor(s)
end
-- function F.SecondsToTime(seconds)
-- local m = seconds / 60
-- local s = seconds % 60
-- return format("%d:%02d", m, s)
-- end
local SEC = _G.SPELL_DURATION_SEC
local MIN = _G.SPELL_DURATION_MIN
local PATTERN_SEC
local PATTERN_MIN
if strfind(SEC, "1f") then
PATTERN_SEC = "%.0"
elseif strfind(SEC, "2f") then
PATTERN_SEC = "%.00"
end
if strfind(MIN, "1f") then
PATTERN_MIN = "%.0"
elseif strfind(MIN, "2f") then
PATTERN_MIN = "%.00"
end
function F.SecondsToTime(seconds)
if seconds > 60 then
return gsub(format(MIN, seconds / 60), PATTERN_MIN, "")
else
return gsub(format(SEC, seconds), PATTERN_SEC, "")
end
end
-------------------------------------------------
-- unit buttons
-------------------------------------------------
local combinedHeader = "CellRaidFrameHeader0"
local separatedHeaders = {"CellRaidFrameHeader1", "CellRaidFrameHeader2", "CellRaidFrameHeader3", "CellRaidFrameHeader4", "CellRaidFrameHeader5", "CellRaidFrameHeader6", "CellRaidFrameHeader7", "CellRaidFrameHeader8"}
-- REVIEW:
-- Cell.clickCastFrames = {}
-- Cell.clickCastFrameQueue = {}
-- function F.RegisterFrame(frame)
-- Cell.clickCastFrames[frame] = true
-- Cell.clickCastFrameQueue[frame] = true -- put into queue
-- Cell.Fire("UpdateQueuedClickCastings")
-- end
-- function F.UnregisterFrame(frame)
-- Cell.clickCastFrames[frame] = nil -- ignore
-- Cell.clickCastFrameQueue[frame] = false -- mark for only cleanup
-- Cell.Fire("UpdateQueuedClickCastings")
-- end
function F.IterateAllUnitButtons(func, updateCurrentGroupOnly, updateQuickAssists, skipShared)
-- solo
if not updateCurrentGroupOnly or (updateCurrentGroupOnly and Cell.vars.groupType == "solo") then
for _, b in pairs(Cell.unitButtons.solo) do
func(b)
end
end
-- party
if not updateCurrentGroupOnly or (updateCurrentGroupOnly and Cell.vars.groupType == "party") then
for index, b in pairs(Cell.unitButtons.party) do
if index ~= "units" then
func(b)
end
end
end
-- raid
if not updateCurrentGroupOnly or (updateCurrentGroupOnly and Cell.vars.groupType == "raid") then
if not updateCurrentGroupOnly or Cell.vars.currentLayoutTable.main.combineGroups then
for _, b in ipairs(Cell.unitButtons.raid[combinedHeader]) do
func(b)
end
end
if not updateCurrentGroupOnly or not Cell.vars.currentLayoutTable.main.combineGroups then
for _, header in ipairs(separatedHeaders) do
for _, b in ipairs(Cell.unitButtons.raid[header]) do
func(b)
end
end
end
-- arena pet
for _, b in pairs(Cell.unitButtons.arena) do
func(b)
end
-- group pet
for index, b in pairs(Cell.unitButtons.pet) do
if index ~= "units" then
func(b)
end
end
end
if not skipShared then
-- npc
for _, b in ipairs(Cell.unitButtons.npc) do
func(b)
end
-- spotlight
for _, b in pairs(Cell.unitButtons.spotlight) do
func(b)
end
end
if Cell.isRetail and updateQuickAssists then
for i = 1, 40 do
func(Cell.unitButtons.quickAssist[i])
end
end
end
function F.IterateSharedUnitButtons(func)
-- npc
for _, b in ipairs(Cell.unitButtons.npc) do
func(b)
end
-- spotlight
for _, b in pairs(Cell.unitButtons.spotlight) do
func(b)
end
end
function F.GetUnitButtonByUnit(unit, getSpotlights, getQuickAssist)
if not unit then return end
local normal, spotlights, quickAssist
if Cell.vars.groupType == "raid" then
if Cell.vars.inBattleground == 5 then
normal = Cell.unitButtons.raid.units[unit] or Cell.unitButtons.npc.units[unit] or Cell.unitButtons.arena[unit]
else
normal = Cell.unitButtons.raid.units[unit] or Cell.unitButtons.npc.units[unit] or Cell.unitButtons.pet.units[unit]
end
elseif Cell.vars.groupType == "party" then
normal = Cell.unitButtons.party.units[unit] or Cell.unitButtons.npc.units[unit]
else -- solo
normal = Cell.unitButtons.solo[unit] or Cell.unitButtons.npc.units[unit]
end
if getSpotlights then
spotlights = {}
for _, b in pairs(Cell.unitButtons.spotlight) do
if b.unit and UnitIsUnit(b.unit, unit) then
tinsert(spotlights, b)
end
end
end
if getQuickAssist then
quickAssist = Cell.unitButtons.quickAssist.units[unit]
end
return normal, spotlights, quickAssist
end
function F.GetUnitButtonByGUID(guid, getSpotlights, getQuickAssist)
return F.GetUnitButtonByUnit(Cell.vars.guids[guid], getSpotlights, getQuickAssist)
end
function F.GetUnitButtonByName(name, getSpotlights, getQuickAssist)
return F.GetUnitButtonByUnit(Cell.vars.names[name], getSpotlights, getQuickAssist)
end
function F.HandleUnitButton(type, unit, func, ...)
if not unit then return end
if type == "guid" then
unit = Cell.vars.guids[unit]
elseif type == "name" then
unit = Cell.vars.names[unit]
end
if not unit then return end
local handled, normal
if Cell.vars.groupType == "raid" then
if Cell.vars.inBattleground == 5 then
normal = Cell.unitButtons.raid.units[unit] or Cell.unitButtons.npc.units[unit] or Cell.unitButtons.arena[unit]
else
normal = Cell.unitButtons.raid.units[unit] or Cell.unitButtons.npc.units[unit] or Cell.unitButtons.pet.units[unit]
end
elseif Cell.vars.groupType == "party" then
normal = Cell.unitButtons.party.units[unit] or Cell.unitButtons.npc.units[unit]
else -- solo
normal = Cell.unitButtons.solo[unit] or Cell.unitButtons.npc.units[unit]
end
if normal then
func(normal, ...)
handled = true
end
for _, b in pairs(Cell.unitButtons.spotlight) do
if b.states.unit and UnitIsUnit(b.states.unit, unit) then
func(b, ...)
handled = true
end
end
return handled
end
function F.UpdateTextWidth(fs, text, width, relativeTo)
if not text or not width then return end
if width == "unlimited" then
fs:SetText(text)
elseif width[1] == "percentage" then
local percent = width[2] or 0.75
local width = relativeTo:GetWidth() - 2
for i = string.utf8len(text), 0, -1 do
fs:SetText(string.utf8sub(text, 1, i))
if fs:GetWidth() / width <= percent then
break
end
end
elseif width[1] == "length" then
if string.len(text) == string.utf8len(text) then -- en
fs:SetText(string.utf8sub(text, 1, width[2]))
else -- non-en
fs:SetText(string.utf8sub(text, 1, width[3]))
end
end
end
function F.GetMarkEscapeSequence(index)
index = index - 1
local left, right, top, bottom
local coordIncrement = 64 / 256
left = mod(index , 4) * coordIncrement
right = left + coordIncrement
top = floor(index / 4) * coordIncrement
bottom = top + coordIncrement
return string.format("|TInterface\\TargetingFrame\\UI-RaidTargetingIcons:0:0:0:0:64:64:%d:%d:%d:%d|t", left*64, right*64, top*64, bottom*64)
end
-- local scriptObjects = {}
-- local frame = CreateFrame("Frame")
-- frame:RegisterEvent("PLAYER_REGEN_DISABLED")
-- frame:RegisterEvent("PLAYER_REGEN_ENABLED")
-- frame:SetScript("OnEvent", function(self, event)
-- if event == "PLAYER_REGEN_ENABLED" then
-- for _, obj in pairs(scriptObjects) do
-- obj:Show()
-- end
-- else
-- for _, obj in pairs(scriptObjects) do
-- obj:Hide()
-- end
-- end
-- end)
-- function F.SetHideInCombat(obj)
-- tinsert(scriptObjects, obj)
-- end
-------------------------------------------------
-- global functions
-------------------------------------------------
local UnitGUID = UnitGUID
local GetNumGroupMembers = GetNumGroupMembers
local GetRaidRosterInfo = GetRaidRosterInfo
local IsInRaid = IsInRaid
local IsInGroup = IsInGroup
local UnitIsPlayer = UnitIsPlayer
local UnitIsUnit = UnitIsUnit
local UnitInParty = UnitInParty
local UnitInRaid = UnitInRaid
local UnitPlayerOrPetInParty = UnitPlayerOrPetInParty
local UnitPlayerOrPetInRaid = UnitPlayerOrPetInRaid
local UnitClass = UnitClass
local UnitClassBase = UnitClassBase
local UnitName = UnitName
local UnitIsGroupLeader = UnitIsGroupLeader
local UnitIsGroupAssistant = UnitIsGroupAssistant
local UnitInPartyIsAI = UnitInPartyIsAI or function() end
-------------------------------------------------
-- frame colors
-------------------------------------------------
local RAID_CLASS_COLORS = RAID_CLASS_COLORS
function F.GetClassColor(class)
if class and class ~= "" and RAID_CLASS_COLORS[class] then
if CUSTOM_CLASS_COLORS and CUSTOM_CLASS_COLORS[class] then
return CUSTOM_CLASS_COLORS[class].r, CUSTOM_CLASS_COLORS[class].g, CUSTOM_CLASS_COLORS[class].b
else
return RAID_CLASS_COLORS[class]:GetRGB()
end
else
return 1, 1, 1
end
end
function F.GetClassColorStr(class)
if class and class ~= "" and RAID_CLASS_COLORS[class] then
if CUSTOM_CLASS_COLORS and CUSTOM_CLASS_COLORS[class] then
return "|c"..CUSTOM_CLASS_COLORS[class].colorStr
else
return "|c"..RAID_CLASS_COLORS[class].colorStr
end
else
return "|cffffffff"
end
end
function F.GetUnitClassColor(unit, class, guid)
class = class or select(2, UnitClass(unit))
guid = guid or UnitGUID(unit)
if UnitIsPlayer(unit) or UnitInPartyIsAI(unit) then -- player
return F.GetClassColor(class)
elseif F.IsPet(guid, unit) then -- pet
return 0.5, 0.5, 1
else -- npc / vehicle
return 0, 1, 0.2
end
end
function F.GetPowerColor(unit)
local r, g, b, t
-- https://wow.gamepedia.com/API_UnitPowerType
local powerType, powerToken, altR, altG, altB = UnitPowerType(unit)
t = powerType
local info = PowerBarColor[powerToken]
if powerType == 0 then -- MANA
info = {r=0, g=0.5, b=1} -- default mana color is too dark!
elseif powerType == 13 then -- INSANITY
info = {r=0.6, g=0.2, b=1}
end
if info then
--The PowerBarColor takes priority
r, g, b = info.r, info.g, info.b
else
if not altR then
-- Couldn't find a power token entry. Default to indexing by power type or just mana if we don't have that either.
info = PowerBarColor[powerType] or PowerBarColor["MANA"]
r, g, b = info.r, info.g, info.b
else
r, g, b = altR, altG, altB
end
end
return r, g, b, t
end
function F.GetPowerBarColor(unit, class)
local r, g, b, lossR, lossG, lossB, t
r, g, b, t = F.GetPowerColor(unit)
if not Cell.loaded then
return r, g, b, r*0.2, g*0.2, b*0.2, t
end
if CellDB["appearance"]["powerColor"][1] == "power_color_dark" then
lossR, lossG, lossB = r, g, b
r, g, b = r*0.2, g*0.2, b*0.2
elseif CellDB["appearance"]["powerColor"][1] == "class_color" then
r, g, b = F.GetClassColor(class)
lossR, lossG, lossB = r*0.2, g*0.2, b*0.2
elseif CellDB["appearance"]["powerColor"][1] == "custom" then
r, g, b = unpack(CellDB["appearance"]["powerColor"][2])
lossR, lossG, lossB = r*0.2, g*0.2, b*0.2
else
lossR, lossG, lossB = r*0.2, g*0.2, b*0.2
end
return r, g, b, lossR, lossG, lossB, t
end
function F.GetHealthBarColor(percent, isDeadOrGhost, r, g, b)
if not Cell.loaded then
return r, g, b, r*0.2, g*0.2, b*0.2
end
local barR, barG, barB, lossR, lossG, lossB
percent = percent or 1
-- bar
if percent == 1 and Cell.vars.useFullColor then
barR = CellDB["appearance"]["fullColor"][2][1]
barG = CellDB["appearance"]["fullColor"][2][2]
barB = CellDB["appearance"]["fullColor"][2][3]
else
if CellDB["appearance"]["barColor"][1] == "class_color" then
barR, barG, barB = r, g, b
elseif CellDB["appearance"]["barColor"][1] == "class_color_dark" then
barR, barG, barB = r*0.2, g*0.2, b*0.2
elseif CellDB["appearance"]["barColor"][1] == "threshold1" then
local c = CellDB["appearance"]["colorThresholds"]
barR, barG, barB = F.ColorThreshold(percent, c[1], c[2], c[3], c[4], c[5], c[6])
elseif CellDB["appearance"]["barColor"][1] == "threshold2" then
local c = CellDB["appearance"]["colorThresholds"]
if percent >= c[5] then
barR, barG, barB = r, g, b -- full: class color
else
barR, barG, barB = F.ColorThreshold(percent, c[1], c[2], {r, g, b}, c[4], c[5], c[6])
end
elseif CellDB["appearance"]["barColor"][1] == "threshold3" then
local c = CellDB["appearance"]["colorThresholds"]
if percent >= c[5] then
barR, barG, barB = r*0.2, g*0.2, b*0.2 -- full: class color
else
barR, barG, barB = F.ColorThreshold(percent, c[1], c[2], {r*0.2, g*0.2, b*0.2}, c[4], c[5], c[6])
end
else
barR = CellDB["appearance"]["barColor"][2][1]
barG = CellDB["appearance"]["barColor"][2][2]
barB = CellDB["appearance"]["barColor"][2][3]
end
end
-- loss
if isDeadOrGhost and Cell.vars.useDeathColor then
lossR = CellDB["appearance"]["deathColor"][2][1]
lossG = CellDB["appearance"]["deathColor"][2][2]
lossB = CellDB["appearance"]["deathColor"][2][3]
else
if CellDB["appearance"]["lossColor"][1] == "class_color" then
lossR, lossG, lossB = r, g, b
elseif CellDB["appearance"]["lossColor"][1] == "class_color_dark" then
lossR, lossG, lossB = r*0.2, g*0.2, b*0.2
elseif CellDB["appearance"]["lossColor"][1] == "threshold1" then
local c = CellDB["appearance"]["colorThresholdsLoss"]
lossR, lossG, lossB = F.ColorThreshold(percent, c[1], c[2], c[3], c[4], c[5], c[6])
elseif CellDB["appearance"]["lossColor"][1] == "threshold2" then
local c = CellDB["appearance"]["colorThresholdsLoss"]
if isDeadOrGhost or percent <= c[4] then
lossR, lossG, lossB = r, g, b -- dead: class color
else
lossR, lossG, lossB = F.ColorThreshold(percent, {r, g, b}, c[2], c[3], c[4], c[5], c[6])
end
elseif CellDB["appearance"]["lossColor"][1] == "threshold3" then
local c = CellDB["appearance"]["colorThresholdsLoss"]
if isDeadOrGhost or percent <= c[4] then
lossR, lossG, lossB = r*0.2, g*0.2, b*0.2 -- dead: class color
else
lossR, lossG, lossB = F.ColorThreshold(percent, {r*0.2, g*0.2, b*0.2}, c[2], c[3], c[4], c[5], c[6])
end
else
lossR = CellDB["appearance"]["lossColor"][2][1]
lossG = CellDB["appearance"]["lossColor"][2][2]
lossB = CellDB["appearance"]["lossColor"][2][3]
end
end
return barR, barG, barB, lossR, lossG, lossB
end
-------------------------------------------------
-- units
-------------------------------------------------
function F.GetNumSubgroupMembers(group)
local n = 0
for i = 1, GetNumGroupMembers() do
local name, _, subgroup = GetRaidRosterInfo(i)
if subgroup == group then
n = n + 1
end
end
return n
end
function F.GetUnitsInSubGroup(group)
local units = {}
for i = 1, GetNumGroupMembers() do
-- name, rank, subgroup, level, class, fileName, zone, online, isDead, role, isML, combatRole = GetRaidRosterInfo(raidIndex)
local name, _, subgroup = GetRaidRosterInfo(i)
if subgroup == group then
tinsert(units, "raid"..i)
end
end
return units
end
function F.GetRaidInfoByName(fullName)
for i = 1, GetNumGroupMembers() do
-- rank: Returns 2 if the raid member is the leader of the raid, 1 if the raid member is promoted to assistant, and 0 otherwise.
local name, rank, subgroup = GetRaidRosterInfo(i)
if name == fullName then
return i, subgroup, rank
end
end
end
function F.GetRaidInfoBySubgroupIndex(group, index)
local currentIndex = 0
for i = 1, GetNumGroupMembers() do
local name, rank, subgroup = GetRaidRosterInfo(i)
if subgroup == group then
currentIndex = currentIndex + 1
if currentIndex == index then
return i, name, rank -- found
end
elseif subgroup > group and currentIndex ~= 0 then
return -- nil if not found
end
end
end
function F.GetPetUnit(playerUnit)
if Cell.vars.groupType == "party" then
if playerUnit == "player" then
return "pet"
else
return "partypet"..select(3, strfind(playerUnit, "^party(%d+)$"))
end
elseif Cell.vars.groupType == "raid" then
return "raidpet"..select(3, strfind(playerUnit, "^raid(%d+)$"))
else
return "pet"
end
end
function F.GetPlayerUnit(petUnit)
if petUnit == "pet" then
return "player"
else
return petUnit:gsub("pet", "")
end
end
function F.IterateGroupMembers()
local groupType = IsInRaid() and "raid" or "party"
local numGroupMembers = GetNumGroupMembers()
local i
if groupType == "party" then
i = 0
numGroupMembers = numGroupMembers - 1
else
i = 1
end
return function()
local ret
if i == 0 then
ret = "player"
elseif i <= numGroupMembers and i > 0 then
ret = groupType .. i
end
i = i + 1
return ret
end
end
function F.IterateGroupPets()
local groupType = IsInRaid() and "raid" or "party"
local numGroupMembers = GetNumGroupMembers()
local i = groupType == "party" and 0 or 1
return function()
local ret
if i == 0 and groupType == "party" then
ret = "pet"
elseif i <= numGroupMembers and i > 0 then
ret = groupType .. "pet" .. i
end
i = i + 1
return ret
end
end
function F.GetGroupType()
if IsInRaid() then
return "raid"
elseif IsInGroup() then
return "party"
else
return "solo"
end
end
function F.UnitInGroup(unit, ignorePets)
if ignorePets then
return UnitIsUnit(unit, "player") or UnitInParty(unit) or UnitInRaid(unit) or UnitInPartyIsAI(unit)
else
return UnitIsUnit(unit, "player") or UnitIsUnit(unit, "pet") or UnitPlayerOrPetInParty(unit) or UnitPlayerOrPetInRaid(unit) or UnitInPartyIsAI(unit)
end
end
-- UnitTokenFromGUID
function F.GetTargetUnitID(target)
if UnitIsUnit(target, "player") then
return "player"
elseif UnitIsUnit(target, "pet") then
return "pet"
end
if not F.UnitInGroup(target) then return end
if UnitIsPlayer(target) or UnitInPartyIsAI(target) then
for unit in F.IterateGroupMembers() do
if UnitIsUnit(target, unit) then
return unit
end
end
else
for unit in F.IterateGroupPets() do
if UnitIsUnit(target, unit) then
return unit
end
end
end
end
function F.GetTargetPetID(target)
if UnitIsUnit(target, "player") then
return "pet"
end
if not F.UnitInGroup(target) then return end
if UnitIsPlayer(target) or UnitInPartyIsAI(target) then
for unit in F.IterateGroupMembers() do
if UnitIsUnit(target, unit) then
return F.GetPetUnit(unit)
end
end
end
end
-- https://wowpedia.fandom.com/wiki/UnitFlag
local OBJECT_AFFILIATION_MINE = 0x00000001
local OBJECT_AFFILIATION_PARTY = 0x00000002
local OBJECT_AFFILIATION_RAID = 0x00000004
function F.IsFriend(unitFlags)
if not unitFlags then return false end
return (bit.band(unitFlags, OBJECT_AFFILIATION_MINE) ~= 0) or (bit.band(unitFlags, OBJECT_AFFILIATION_RAID) ~= 0) or (bit.band(unitFlags, OBJECT_AFFILIATION_PARTY) ~= 0)
end
function F.IsPlayer(guid)
if guid then
return string.find(guid, "^Player")
end
end
function F.IsPet(guid, unit)
if unit then
return strfind(unit, "pet%d*$")
end
if guid then
return string.find(guid, "^Pet")
end
end
function F.IsNPC(guid)
if guid then
return string.find(guid, "^Creature")
end
end
function F.IsVehicle(guid)
if guid then
return string.find(guid, "^Vehicle")
end
end
function F.GetTargetUnitInfo()
if UnitIsUnit("target", "player") then
return "player", UnitName("player"), UnitClassBase("player")
elseif UnitIsUnit("target", "pet") then
return "pet", UnitName("pet")
end
if not F.UnitInGroup("target") then return end
if IsInRaid() then
for i = 1, GetNumGroupMembers() do
if UnitIsUnit("target", "raid"..i) then
return "raid"..i, UnitName("raid"..i), UnitClassBase("raid"..i)
end
if UnitIsUnit("target", "raidpet"..i) then
return "raidpet"..i, UnitName("raidpet"..i)
end
end
elseif IsInGroup() then
for i = 1, GetNumGroupMembers()-1 do
if UnitIsUnit("target", "party"..i) then
return "party"..i, UnitName("party"..i), UnitClassBase("party"..i)
end
if UnitIsUnit("target", "partypet"..i) then
return "partypet"..i, UnitName("partypet"..i)
end
end
end
end
function F.HasPermission(isPartyMarkPermission)
if isPartyMarkPermission and IsInGroup() and not IsInRaid() then return true end
return UnitIsGroupLeader("player") or (IsInRaid() and UnitIsGroupAssistant("player"))
end
-------------------------------------------------
-- LibSharedMedia
-------------------------------------------------
Cell.vars.texture = "Interface\\AddOns\\Cell\\Media\\statusbar.tga"
Cell.vars.emptyTexture = "Interface\\AddOns\\Cell\\Media\\empty.tga"
Cell.vars.whiteTexture = "Interface\\AddOns\\Cell\\Media\\white.tga"
local LSM = LibStub("LibSharedMedia-3.0", true)
LSM:Register("statusbar", "Cell ".._G.DEFAULT, Cell.vars.texture)
LSM:Register("font", "visitor", [[Interface\Addons\Cell\Media\Fonts\visitor.ttf]], 255)
function F.GetBarTexture()
--! update Cell.vars.texture for further use in UnitButton_OnLoad
if LSM:IsValid("statusbar", CellDB["appearance"]["texture"]) then
Cell.vars.texture = LSM:Fetch("statusbar", CellDB["appearance"]["texture"])
else
Cell.vars.texture = "Interface\\AddOns\\Cell\\Media\\statusbar.tga"
end
return Cell.vars.texture
end
function F.GetBarTextureByName(name)
if LSM:IsValid("statusbar", name) then
return LSM:Fetch("statusbar", name)
end
return "Interface\\AddOns\\Cell\\Media\\statusbar.tga"
end
function F.GetFont(font)
if font and LSM:IsValid("font", font) then
return LSM:Fetch("font", font)
elseif type(font) == "string" and strfind(strlower(font), ".ttf$") then
return font
else
if CellDB["appearance"]["useGameFont"] then
return GameFontNormal:GetFont()
else
return "Interface\\AddOns\\Cell\\Media\\Fonts\\Accidental_Presidency.ttf"
end
end
end
local defaultFontName = "Cell ".._G.DEFAULT
local defaultFont
function F.GetFontItems()
if CellDB["appearance"]["useGameFont"] then
defaultFont = GameFontNormal:GetFont()
else
defaultFont = "Interface\\AddOns\\Cell\\Media\\Fonts\\Accidental_Presidency.ttf"
end
local items = {}
local fonts, fontNames
-- if LSM then
fonts, fontNames = F.Copy(LSM:HashTable("font")), F.Copy(LSM:List("font"))
-- insert default font
tinsert(fontNames, 1, defaultFontName)
fonts[defaultFontName] = defaultFont
for _, name in pairs(fontNames) do
tinsert(items, {
["text"] = name,
["font"] = fonts[name],
-- ["onClick"] = function()
-- CellDB["appearance"]["font"] = name
-- Cell.Fire("UpdateAppearance", "font")
-- end,
})
end
-- else
-- fontNames = {defaultFontName}
-- fonts = {[defaultFontName] = defaultFont}
-- tinsert(items, {
-- ["text"] = defaultFontName,
-- ["font"] = defaultFont,
-- -- ["onClick"] = function()
-- -- CellDB["appearance"]["font"] = defaultFontName
-- -- Cell.Fire("UpdateAppearance", "font")
-- -- end,
-- })
-- end
return items, fonts, defaultFontName, defaultFont
end
-------------------------------------------------
-- texture
-------------------------------------------------
function F.GetTexCoord(width, height)
-- ULx,ULy, LLx,LLy, URx,URy, LRx,LRy
local texCoord = {0.12, 0.12, 0.12, 0.88, 0.88, 0.12, 0.88, 0.88}
local aspectRatio = width / height
local xRatio = aspectRatio < 1 and aspectRatio or 1
local yRatio = aspectRatio > 1 and 1 / aspectRatio or 1
for i, coord in ipairs(texCoord) do
local aspectRatio = (i % 2 == 1) and xRatio or yRatio
texCoord[i] = (coord - 0.5) * aspectRatio + 0.5
end
return texCoord
end
-- function F.RotateTexture(tex, degrees)
-- local angle = math.rad(degrees)
-- local cos, sin = math.cos(angle), math.sin(angle)
-- tex:SetTexCoord((sin - cos), -(cos + sin), -cos, -sin, sin, -cos, 0, 0)
-- end
-- https://wowpedia.fandom.com/wiki/Applying_affine_transformations_using_SetTexCoord
local s2 = sqrt(2)
local function CalculateCorner(degrees)
local r = math.rad(degrees)
return 0.5 + math.cos(r) / s2, 0.5 + math.sin(r) / s2
end
function F.RotateTexture(texture, degrees)
local LRx, LRy = CalculateCorner(degrees + 45)
local LLx, LLy = CalculateCorner(degrees + 135)
local ULx, ULy = CalculateCorner(degrees + 225)
local URx, URy = CalculateCorner(degrees - 45)
texture:SetTexCoord(ULx, ULy, LLx, LLy, URx, URy, LRx, LRy)
end
-- wow atlases
local wowAtlases = {
"playerpartyblip",
"Artifacts-PerkRing-WhiteGlow",
"AftLevelup-WhiteIconGlow",
"LootBanner-IconGlow",
"AftLevelup-WhiteStarBurst",
"ChallengeMode-WhiteSpikeyGlow",
"UI-QuestPoiCampaign-OuterGlow",
"vignettekill",
"PetJournal-FavoritesIcon",
"dungeonskull",
"questnormal",
"questturnin",
"bags-icon-addslots",
"communities-chat-icon-plus",
"communities-chat-icon-minus",
}
-- wow textures
local wowTextures = {
}
-- shapes
local shapes = {
"circle_blurred",
"circle_filled",
"circle_thin",
"circle",
"heart_filled",
"heart",
"rhombus",
"rhombus_filled",
"square_filled",
"square",
"star_filled",
"star",
"starburst_filled",
"starburst",
"triangle_filled",
"triangle",
}
-- weakauras
local powaTextures = {
9, 10, 12, 13, 14, 15, 21, 22, 25, 27, 29,
37, 38, 39, 40, 41, 42, 43, 44,
49, 51, 52, 53, 58, 78, 118, 84,
96, 97, 98, 99, 100, 114, 115, 116, 132, 138, 143
}
function F.GetTextures()
local builtIns = #wowAtlases + #wowTextures + #shapes
local t = {}
-- wow atlases
for _, wa in pairs(wowAtlases) do
tinsert(t, wa)
end
-- wow textures
for _, wt in pairs(wowTextures) do
tinsert(t, wt)
end
-- built-ins
for _, s in pairs(shapes) do
tinsert(t, "Interface\\AddOns\\Cell\\Media\\Shapes\\"..s..".tga")
end
-- add weakauras textures
if WeakAuras then
builtIns = builtIns + #powaTextures
for _, powa in pairs(powaTextures) do
tinsert(t, "Interface\\AddOns\\WeakAuras\\PowerAurasMedia\\Auras\\Aura"..powa..".tga")
end
end
-- customs
for _, path in pairs(CellDB["customTextures"]) do
tinsert(t, path)
end
return builtIns, t
end
function F.GetDefaultRoleIcon(role)
if not role or role == "NONE" then return "" end
return "Interface\\AddOns\\Cell\\Media\\Roles\\Default_" .. role
end
function F.GetDefaultRoleIconEscapeSequence(role, size)
if not role or role == "NONE" then return "" end
return "|TInterface\\AddOns\\Cell\\Media\\Roles\\Default_" .. role .. ":" .. (size or 0) .. "|t"
end
-------------------------------------------------
-- frame
-------------------------------------------------
function F.GetMouseFocus()
if GetMouseFoci then
return GetMouseFoci()[1]
else
return GetMouseFocus()
end
end
-------------------------------------------------
-- instance
-------------------------------------------------
function F.GetInstanceName()
if IsInInstance() then
local name = GetInstanceInfo()
if not name then name = GetRealZoneText() end
return name
else
local mapID = C_Map.GetBestMapForUnit("player")
if type(mapID) ~= "number" or mapID < 1 then
return ""
end
local info = MapUtil.GetMapParentInfo(mapID, Enum.UIMapType.Continent, true)
if info then
return info.name, info.mapID
end
return ""
end
end
-------------------------------------------------
-- spell
-------------------------------------------------
-- https://wow.gamepedia.com/UIOBJECT_GameTooltip
-- local function EnumerateTooltipLines_helper(...)
-- for i = 1, select("#", ...) do
-- local region = select(i, ...)
-- if region and region:GetObjectType() == "FontString" then
-- local text = region:GetText() -- string or nil
-- print(region:GetName(), text)
-- end
-- end
-- end
-- https://wowpedia.fandom.com/wiki/Patch_10.0.2/API_changes
local lines = {}
function F.GetSpellTooltipInfo(spellId)
wipe(lines)
local name, icon = F.GetSpellInfo(spellId)
if not name then return end
local data = C_TooltipInfo.GetSpellByID(spellId)
for i, line in ipairs(data.lines) do
TooltipUtil.SurfaceArgs(line)
-- line.leftText
-- line.rightText
end
return name, icon, table.concat(lines, "\n")
end
if Cell.isRetail or Cell.isMists then
local GetSpellInfo = C_Spell.GetSpellInfo
local GetSpellTexture = C_Spell.GetSpellTexture
function F.GetSpellInfo(spellId)
if not spellId then return end
local info = GetSpellInfo(spellId)
if not info then return end
if not info.iconID then -- when?
info.iconID = GetSpellTexture(spellId)
end
return info.name, info.iconID
end
else
local GetSpellInfo = GetSpellInfo
function F.GetSpellInfo(spellId)
if not spellId then return end
local rank
spellId, rank = strsplit(":", spellId)
local name, _, icon = GetSpellInfo(spellId)
return name, icon, tonumber(rank)
end
end
if Cell.isWrath or Cell.isVanilla then
local GetSpellInfo = GetSpellInfo
local GetNumSpellTabs = GetNumSpellTabs
local GetSpellTabInfo = GetSpellTabInfo
local GetSpellBookItemName = GetSpellBookItemName
local PATTERN = TRADESKILL_RANK_HEADER:gsub(" ", ""):gsub("%%d", "%%s*(%%d+)")
function F.GetMaxSpellRank(spellId)
local spellName = select(1, GetSpellInfo(spellId))
if not spellName then return end
local maxRank = 0
local bookType = BOOKTYPE_SPELL
local totalSpells = 0
for tab = 1, GetNumSpellTabs() do
local name, texture, offset, numSpells = GetSpellTabInfo(tab)
totalSpells = totalSpells + numSpells
end
for i = 1, totalSpells do
local name, subText = GetSpellBookItemName(i, bookType)
if name == spellName and subText then
local rank = tonumber(subText:match(PATTERN))
if rank and rank > maxRank then
maxRank = rank
end
end
end
return maxRank
end
end
if C_Spell.GetSpellCooldown then
local GetSpellCooldown = C_Spell.GetSpellCooldown
F.GetSpellCooldown = function(spellId)
local info = GetSpellCooldown(spellId)
if info then
return info.startTime, info.duration
end
end
else
F.GetSpellCooldown = function(spellId)
local start, duration = GetSpellCooldown(spellId)
return start, duration
end
end
function F.IsSpellReady(spellId)
local start, duration = F.GetSpellCooldown(spellId)
if start == 0 or duration == 0 then
return true
else
local _, gcd = F.GetSpellCooldown(61304) --! check gcd
if duration == gcd then -- spell ready
return true
else
local cdLeft = start + duration - GetTime()
return false, cdLeft
end
end
end
-------------------------------------------------
-- macro
-------------------------------------------------
local mc = CreateFrame("Frame")
mc:RegisterEvent("UPDATE_MACROS")
local macroIndices = {}
mc:SetScript("OnEvent", function()
wipe(macroIndices)
local global, perChar = GetNumMacros()
for i = 1, global do
tinsert(macroIndices, i)
end
for i = 1, perChar do
tinsert(macroIndices, 120 + i)
end
end)
function F.GetMacroIndices()
return macroIndices
end
-------------------------------------------------
-- auras
-------------------------------------------------
-- name, icon, count, debuffType, duration, expirationTime, source, isStealable, nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, castByPlayer, nameplateShowAll, timeMod = UnitAura
-- NOTE: FrameXML/AuraUtil.lua
-- AuraUtil.FindAura(predicate, unit, filter, predicateArg1, predicateArg2, predicateArg3)
-- predicate(predicateArg1, predicateArg2, predicateArg3, ...)
local function predicate(...)
local idToFind = ...
local id = select(13, ...)
return idToFind == id
end
function F.FindAuraById(unit, type, spellId)
if type == "BUFF" then
return AuraUtil.FindAura(predicate, unit, "HELPFUL", spellId)
else
return AuraUtil.FindAura(predicate, unit, "HARMFUL", spellId)
end
end
if Cell.isRetail then
function F.FindDebuffByIds(unit, spellIds)
local debuffs = {}
AuraUtil.ForEachAura(unit, "HARMFUL", nil, function(name, icon, count, debuffType, duration, expirationTime, source, isStealable, nameplateShowPersonal, spellId)
if spellIds[spellId] then
debuffs[spellId] = I.CheckDebuffType(debuffType, spellId)
end
end)
return debuffs
end
function F.FindAuraByDebuffTypes(unit, types)
local debuffs = {}
AuraUtil.ForEachAura(unit, "HARMFUL", nil, function(name, icon, count, debuffType, duration, expirationTime, source, isStealable, nameplateShowPersonal, spellId)
if types == "all" or types[debuffType] then
debuffs[spellId] = I.CheckDebuffType(debuffType, spellId)
end
end)
return debuffs
end
else
function F.FindDebuffByIds(unit, spellIds)
local debuffs = {}
for i = 1, 40 do
local name, icon, count, debuffType, duration, expirationTime, source, isStealable, nameplateShowPersonal, spellId = UnitDebuff(unit, i)
if not name then
break
end
if spellIds[spellId] then
debuffs[spellId] = I.CheckDebuffType(debuffType, spellId)
end
end
return debuffs
end
function F.FindAuraByDebuffTypes(unit, types)
local debuffs = {}
for i = 1, 40 do
local name, icon, count, debuffType, duration, expirationTime, source, isStealable, nameplateShowPersonal, spellId = UnitDebuff(unit, i)
if not name then
break
end
if types == "all" or types[debuffType] then
debuffs[spellId] = I.CheckDebuffType(s, spellId)
end
end
return debuffs
end
end
-------------------------------------------------
-- OmniCD
-------------------------------------------------
function F.UpdateOmniCDPosition(frame)
if OmniCD and OmniCD[1].db.position.uf == frame then
C_Timer.After(0.5, function()
OmniCD[1].Party:UpdatePosition()
end)
end
end
-------------------------------------------------
-- LibGetFrame
-------------------------------------------------
local frame_priorities = {}
local inited_priorities = {}
local modified_priorities = {}
local spotlightPriorityEnabled
local quickAssistPriorityEnabled
function F.UpdateFramePriority()
wipe(frame_priorities)
wipe(modified_priorities)
spotlightPriorityEnabled = nil
quickAssistPriorityEnabled = nil
for i, t in pairs(CellDB["general"]["framePriority"]) do
if t[2] then
if t[1] == "Main" then
tinsert(frame_priorities, i, "^CellNormalUnitFrame$")
elseif t[1] == "Spotlight" then
tinsert(frame_priorities, i, "^CellSpotlightUnitFrame$")
spotlightPriorityEnabled = true
else
tinsert(frame_priorities, i, "^CellQuickAssistUnitFrame$")
quickAssistPriorityEnabled = true
end
else
tinsert(frame_priorities, i, "^CellPlaceholder$")
end
end
F.Debug(frame_priorities)
end
function Cell.GetUnitFramesForLGF(unit, frames, priorities)
frames = frames or {}
local normal, spotlights, quickAssist = F.GetUnitButtonByUnit(unit, spotlightPriorityEnabled, quickAssistPriorityEnabled)
if normal then
frames[normal.widgets.highLevelFrame] = "CellNormalUnitFrame"
end
if spotlights then
-- for _, spotlight in pairs(spotlights) do
-- if not strfind(spotlight.unit, "target$") and spotlight.widgets and spotlight.widgets.highLevelFrame then
-- frames[spotlight.widgets.highLevelFrame] = "CellSpotlightUnitFrame"
-- break
-- end
-- end
--! just use the first (can be "XXtarget", whatever)
if spotlights[1] then
frames[spotlights[1].widgets.highLevelFrame] = "CellSpotlightUnitFrame"
end
end
if quickAssist then
frames[quickAssist] = "CellQuickAssistUnitFrame"
end
if not inited_priorities[priorities] then
inited_priorities[priorities] = true
for i = 1, 3 do
tinsert(priorities, i, "^CellPlaceholder$")
end
end
if not modified_priorities[priorities] then
modified_priorities[priorities] = true
for i, p in ipairs(frame_priorities) do
priorities[i] = p
end
end
return frames
end
-------------------------------------------------
-- range check
-------------------------------------------------
local UnitIsVisible = UnitIsVisible
local UnitInRange = UnitInRange
local UnitCanAssist = UnitCanAssist
local UnitCanAttack = UnitCanAttack
local UnitCanCooperate = UnitCanCooperate
local IsSpellInRange = (C_Spell and C_Spell.IsSpellInRange) and C_Spell.IsSpellInRange or IsSpellInRange
local IsItemInRange = (C_Spell and C_Item.IsItemInRange) and C_Item.IsItemInRange or IsItemInRange
local CheckInteractDistance = CheckInteractDistance
local UnitIsDead = UnitIsDead
local IsSpellKnownOrOverridesKnown = IsSpellKnownOrOverridesKnown
-- local GetSpellTabInfo = GetSpellTabInfo
-- local GetNumSpellTabs = GetNumSpellTabs
-- local GetSpellBookItemName = GetSpellBookItemName
-- local BOOKTYPE_SPELL = BOOKTYPE_SPELL
local UnitInSamePhase
if Cell.isRetail then
UnitInSamePhase = function(unit)
return not UnitPhaseReason(unit)
end
else
UnitInSamePhase = UnitInPhase
end
local playerClass = UnitClassBase("player")
local friendSpells = {
-- ["DEATHKNIGHT"] = 47541,
-- ["DEMONHUNTER"] = ,
["DRUID"] = (Cell.isWrath or Cell.isVanilla) and 5185 or 8936, -- 治疗之触 / 愈合
-- FIXME: [361469 活化烈焰] 会被英雄天赋 [431443 时序烈焰] 替代,但它而且有问题
-- IsSpellInRange 始终返回 nil
["EVOKER"] = 355913, -- 翡翠之花
-- ["HUNTER"] = 136,
["MAGE"] = 1459, -- 奥术智慧 / 奥术光辉
["MONK"] = 116670, -- 活血术
["PALADIN"] = Cell.isRetail and 19750 or 635, -- 圣光闪现 / 圣光术
["PRIEST"] = (Cell.isWrath or Cell.isVanilla) and 2050 or 2061, -- 次级治疗术 / 快速治疗
-- ["ROGUE"] = Cell.isWrath and 57934,
["SHAMAN"] = Cell.isRetail and 8004 or 331, -- 治疗之涌 / 治疗波
["WARLOCK"] = 5697, -- 无尽呼吸
-- ["WARRIOR"] = 3411,
}
local deadSpells = {
["EVOKER"] = 361227, -- resurrection range, need separately for evoker
}
local petSpells = {
["HUNTER"] = 136,
}
local harmSpells = {
["DEATHKNIGHT"] = 47541, -- 凋零缠绕
["DEMONHUNTER"] = 185123, -- 投掷利刃
["DRUID"] = 5176, -- 愤怒
-- FIXME: [361469 活化烈焰] 会被英雄天赋 [431443 时序烈焰] 替代,但它而且有问题
-- IsSpellInRange 始终返回 nil
["EVOKER"] = 362969, -- 碧蓝打击
["HUNTER"] = 75, -- 自动射击
["MAGE"] = Cell.isRetail and 116 or 133, -- 寒冰箭 / 火球术
["MONK"] = 117952, -- 碎玉闪电
["PALADIN"] = 20271, -- 审判
["PRIEST"] = Cell.isRetail and 589 or 585, -- 暗言术:痛 / 惩击
["ROGUE"] = 1752, -- 影袭
["SHAMAN"] = Cell.isRetail and 188196 or 403, -- 闪电箭
["WARLOCK"] = 234153, -- 吸取生命
["WARRIOR"] = 355, -- 嘲讽
}
-- local friendItems = {
-- ["DEATHKNIGHT"] = 34471,
-- ["DEMONHUNTER"] = 34471,
-- ["DRUID"] = 34471,
-- ["EVOKER"] = 1180, -- 30y
-- ["HUNTER"] = 34471,
-- ["MAGE"] = 34471,
-- ["MONK"] = 34471,
-- ["PALADIN"] = 34471,
-- ["PRIEST"] = 34471,
-- ["ROGUE"] = 34471,
-- ["SHAMAN"] = 34471,
-- ["WARLOCK"] = 34471,
-- ["WARRIOR"] = 34471,
-- }
local harmItems = {
["DEATHKNIGHT"] = 28767, -- 40y
["DEMONHUNTER"] = 28767, -- 40y
["DRUID"] = 28767, -- 40y
["EVOKER"] = 24268, -- 25y
["HUNTER"] = 28767, -- 40y
["MAGE"] = 28767, -- 40y
["MONK"] = 28767, -- 40y
["PALADIN"] = 835, -- 30y
["PRIEST"] = 28767, -- 40y
["ROGUE"] = 28767, -- 40y
["SHAMAN"] = 28767, -- 40y
["WARLOCK"] = 28767, -- 40y
["WARRIOR"] = 28767, -- 40y
}
-- local FindSpellIndex
-- if C_SpellBook and C_SpellBook.FindSpellBookSlotForSpell then
-- FindSpellIndex = function(spellName)
-- if not spellName or spellName == "" then return end
-- return C_SpellBook.FindSpellBookSlotForSpell(spellName)
-- end
-- else
-- local function GetNumSpells()
-- local _, _, offset, numSpells = GetSpellTabInfo(GetNumSpellTabs())
-- return offset + numSpells
-- end
-- FindSpellIndex = function(spellName)
-- if not spellName or spellName == "" then return end
-- for i = 1, GetNumSpells() do
-- local spell = GetSpellBookItemName(i, BOOKTYPE_SPELL)
-- if spell == spellName then
-- return i
-- end
-- end
-- end
-- end
local UnitInSpellRange
if C_Spell and C_Spell.IsSpellInRange then
UnitInSpellRange = function(spellName, unit)
return IsSpellInRange(spellName, unit)
end
else
UnitInSpellRange = function(spellName, unit)
return IsSpellInRange(spellName, unit) == 1
end
end
local rc = CreateFrame("Frame")
rc:RegisterEvent("SPELLS_CHANGED")
local spell_friend, spell_pet, spell_harm, spell_dead
CELL_RANGE_CHECK_FRIENDLY = {}
CELL_RANGE_CHECK_HOSTILE = {}
CELL_RANGE_CHECK_DEAD = {}
CELL_RANGE_CHECK_PET = {}
local function SPELLS_CHANGED()
spell_friend = CELL_RANGE_CHECK_FRIENDLY[playerClass] or friendSpells[playerClass]
spell_harm = CELL_RANGE_CHECK_HOSTILE[playerClass] or harmSpells[playerClass]
spell_dead = CELL_RANGE_CHECK_DEAD[playerClass] or deadSpells[playerClass]
spell_pet = CELL_RANGE_CHECK_PET[playerClass] or petSpells[playerClass]
if spell_friend and IsSpellKnownOrOverridesKnown(spell_friend) then
spell_friend = F.GetSpellInfo(spell_friend)
else
spell_friend = nil
end
if spell_harm and IsSpellKnownOrOverridesKnown(spell_harm) then
spell_harm = F.GetSpellInfo(spell_harm)
else
spell_harm = nil
end
if spell_dead and IsSpellKnownOrOverridesKnown(spell_dead) then
spell_dead = F.GetSpellInfo(spell_dead)
else
spell_dead = nil
end
if spell_pet and IsSpellKnownOrOverridesKnown(spell_pet) then
spell_pet = F.GetSpellInfo(spell_pet)
else
spell_pet = nil
end
-- F.Debug(
-- "[RANGE CHECK]",
-- "\nfriend:", spell_friend or "nil",
-- "\npet:", spell_pet or "nil",
-- "\nharm:", spell_harm or "nil",
-- "\ndead:", spell_dead or "nil"
-- )
end
local timer
local function DELAYED_SPELLS_CHANGED()
if timer then timer:Cancel() end
timer = C_Timer.NewTimer(1, SPELLS_CHANGED)
end
rc:SetScript("OnEvent", DELAYED_SPELLS_CHANGED)
function F.IsInRange(unit, check)
if not UnitIsVisible(unit) then
return false
end
if UnitIsUnit("player", unit) then
return true
elseif not check and F.UnitInGroup(unit) then
-- NOTE: UnitInRange only works with group players/pets
--! but not available for PLAYER PET when SOLO
local inRange, checked = UnitInRange(unit)
if not checked then
return F.IsInRange(unit, true)
end
return inRange
else
if UnitCanAssist("player", unit) then -- or UnitCanCooperate("player", unit)
if not (UnitIsConnected(unit) and UnitInSamePhase(unit)) then
return false
end
if UnitIsDead(unit) then
if spell_dead then
return UnitInSpellRange(spell_dead, unit)
end
elseif spell_friend then
return UnitInSpellRange(spell_friend, unit)
end
local inRange, checked = UnitInRange(unit)
if checked then
return inRange
end
if UnitIsUnit(unit, "pet") and spell_pet then
-- no spell_friend, use spell_pet
return UnitInSpellRange(spell_pet, unit)
end
elseif UnitCanAttack("player", unit) then
if UnitIsDead(unit) then
return CheckInteractDistance(unit, 4) -- 28 yards
elseif spell_harm then
return UnitInSpellRange(spell_harm, unit)
end
return IsItemInRange(harmItems[playerClass], unit)
end
if not InCombatLockdown() then
return CheckInteractDistance(unit, 4) -- 28 yards
end
return true
end
end
-------------------------------------------------
-- RangeCheck debug
-------------------------------------------------
local debug = CreateFrame("Frame", "CellRangeCheckDebug", CellParent, "BackdropTemplate")
debug:SetBackdrop({bgFile = Cell.vars.whiteTexture})
debug:SetBackdropColor(0.1, 0.1, 0.1, 0.9)
debug:SetBackdropBorderColor(0, 0, 0, 1)
debug:SetPoint("LEFT", 300, 0)
debug:Hide()
debug.text = debug:CreateFontString(nil, "OVERLAY")
debug.text:SetFont(GameFontNormal:GetFont(), 13, "")
debug.text:SetShadowColor(0, 0, 0)
debug.text:SetShadowOffset(1, -1)
debug.text:SetJustifyH("LEFT")
debug.text:SetSpacing(5)
debug.text:SetPoint("LEFT", 5, 0)
local function GetResult1()
local inRange, checked = UnitInRange("target")
return "UnitID: " .. (F.GetTargetUnitID("target") or "target") ..
"\n|cffffff00F.IsInRange:|r " .. (F.IsInRange("target") and "true" or "false") ..
"\nUnitInRange: " .. (checked and "checked" or "unchecked") .. " " .. (inRange and "true" or "false") ..
"\nUnitIsVisible: " .. (UnitIsVisible("target") and "true" or "false") ..
"\n\nUnitCanAssist: " .. (UnitCanAssist("player", "target") and "true" or "false") ..
"\nUnitCanCooperate: " .. (UnitCanCooperate("player", "target") and "true" or "false") ..
"\nUnitCanAttack: " .. (UnitCanAttack("player", "target") and "true" or "false") ..
"\n\nUnitIsConnected: " .. (UnitIsConnected("target") and "true" or "false") ..
"\nUnitInSamePhase: " .. (UnitInSamePhase("target") and "true" or "false") ..
"\nUnitIsDead: " .. (UnitIsDead("target") and "true" or "false") ..
"\n\nspell_friend: " .. (spell_friend and (spell_friend .. " " .. (UnitInSpellRange(spell_friend, "target") and "true" or "false")) or "none") ..
"\nspell_dead: " .. (spell_dead and (spell_dead .. " " .. (UnitInSpellRange(spell_dead, "target") and "true" or "false")) or "none") ..
"\nspell_pet: " .. (spell_pet and (spell_pet .. " " .. (UnitInSpellRange(spell_pet, "target") and "true" or "false")) or "none") ..
"\nspell_harm: " .. (spell_harm and (spell_harm .. " " .. (UnitInSpellRange(spell_harm, "target") and "true" or "false")) or "none")
end
local function GetResult2()
if UnitCanAttack("player", "target") then
return "IsItemInRange: " .. (IsItemInRange(harmItems[playerClass], "target") and "true" or "false") ..
"\nCheckInteractDistance(28y): " .. (CheckInteractDistance("target", 4) and "true" or "false")
else
return "IsItemInRange: " .. (InCombatLockdown() and "notAvailable" or (IsItemInRange(harmItems[playerClass], "target") and "true" or "false")) ..
"\nCheckInteractDistance(28y): " .. (InCombatLockdown() and "notAvailable" or (CheckInteractDistance("target", 4) and "true" or "false"))
end
end
debug:SetScript("OnUpdate", function(self, elapsed)
self.elapsed = (self.elapsed or 0) + elapsed
if self.elapsed >= 0.25 then
self.elapsed = 0
local result = GetResult1() .. "\n\n" .. GetResult2()
result = string.gsub(result, "none", "|cffabababnone|r")
result = string.gsub(result, "true", "|cff00ff00true|r")
result = string.gsub(result, "false", "|cffff0000false|r")
result = string.gsub(result, " checked", " |cff00ff00checked|r")
result = string.gsub(result, "unchecked", "|cffff0000unchecked|r")
debug.text:SetText("|cffff0066Cell Range Check (Target)|r\n\n" .. result)
debug:SetSize(debug.text:GetStringWidth() + 10, debug.text:GetStringHeight() + 20)
end
end)
debug:SetScript("OnEvent", function()
if not UnitExists("target") then
debug:Hide()
return
end
debug:Show()
end)
SLASH_CELLRC1 = "/cellrc"
function SlashCmdList.CELLRC()
if debug:IsEventRegistered("PLAYER_TARGET_CHANGED") then
debug:UnregisterEvent("PLAYER_TARGET_CHANGED")
debug:Hide()
else
debug:RegisterEvent("PLAYER_TARGET_CHANGED")
if UnitExists("target") then
debug:Show()
end
end
end
---------------------------------------------------------------------
-- spec data
---------------------------------------------------------------------
if Cell.isMists then
end