--- /dev/null
+-- Cooldowns.lua
+local _;
+local pairs, ipairs, unpack = pairs, ipairs, unpack;
+local tremove, format, ssub = table.remove, string.format, string.sub;
+local floor, strsplit = math.floor, strsplit;
+local GetTime, UnitGUID, UnitClass = GetTime, UnitGUID, UnitClass;
+local UnitName, GetSpellTexture = UnitName, GetSpellTexture;
+local IsInGroup, IsInRaid = IsInGroup, IsInRaid;
+local CreateFrame, CTimerAfter = CreateFrame, C_Timer.After;
+local UnitIsUnit, IsEncounterInProgress = UnitIsUnit, IsEncounterInProgress;
+local UnitExists, UnitGroupRolesAssigned = UnitExists, UnitGroupRolesAssigned;
+local UnitIsDeadOrGhost, UnitIsConnected = UnitIsDeadOrGhost, UnitIsConnected;
+local GetTalentInfo, GetInspectSpecialization = GetTalentInfo, GetInspectSpecialization;
+local GetSpecializationInfo, GetSpecialization = GetSpecializationInfo, GetSpecialization;
+local GetInventoryItemLink, GetInventoryItemID = GetInventoryItemLink, GetInventoryItemID;
+local CanInspect, NotifyInspect, ClearInspectPlayer = CanInspect, NotifyInspect, ClearInspectPlayer;
+local INVSLOT_MAINHAND, INVSLOT_WRIST = INVSLOT_MAINHAND, INVSLOT_WRIST;
+
+local upColor = {0, 0.7, 0.2, 0.7};
+local downColor = {1, 0, 0, 0.5};
+
+local cdframe = CreateFrame("Frame", "OmaCD", UIParent);
+local frames = {};
+local unused = {};
+local shown = {}; -- simple array of frames in shown order
+
+local specs = {
+ ["PALADIN"] = 65,
+ ["PRIEST"] = false, -- priest has 2 healing specs
+ ["SHAMAN"] = 264,
+ ["MONK"] = 270,
+ ["DRUID"] = 105,
+};
+local trackedcds = {
+ [65] = { -- Holy Paladin
+ [31821] = 180, -- Aura Mastery
+ },
+ [105] = { -- Resto Druid
+ [740] = 180, -- Tranquility
+ },
+ [256] = { -- Discipline Priest
+ [62618] = 180, -- Power Word: Barrier
+ },
+ [257] = { -- Holy Priest
+ [64843] = 180, -- Divine Hymn
+ },
+ [264] = { -- Resto Shaman
+ [108280] = 180, -- Healing Tide Totem
+ [98008] = 180, -- Spirit Link Totem
+ },
+ [270] = { -- Mistweaver Monk
+ [115310] = (180-40), -- Revival, already lowered by Artifact when lvl 75
+ },
+};
+local monkRelics = { -- Tendrils of Revival
+ ["151012"] = -10,
+ ["151010"] = -10,
+ ["147112"] = -10,
+ ["152291"] = -10,
+};
+
+local cds = {}; -- CD durations currently available modified by passive cdfixes
+-- CD fixes active e.g. cdfixes[guid] = -60, no need for spellid as
+-- only classes with one CD have fixes
+local cdfixes = {};
+local runningcds = {}; -- CDs currently active
+
+local idToGuid = {};
+local guidToId = {};
+local guidToSpecid = {};
+local monksWithLegendary = {};
+local inspectSent = {};
+local dead = {};
+local disconnected = {};
+
+local function CdUp(guid, spellid)
+ local frame = frames[guid][spellid];
+ if runningcds[guid] then runningcds[guid][spellid] = nil end
+ frame.base:SetVertexColor(unpack(upColor));
+ frame.text:SetText(frame.name);
+end
+
+local function CdDown(guid, spellid, start)
+ local frame = frames[guid][spellid];
+ if not runningcds[guid] then runningcds[guid] = {} end
+ runningcds[guid][spellid] = start;
+ frame.base:SetVertexColor(unpack(downColor));
+ if start then frame.text:SetText(cds[guid][spellid]) end
+end
+
+local function sendInspect(guid, id, func)
+ if not inspectSent[guid] and CanInspect(id) then
+ inspectSent[guid] = true;
+ local _, _, _, _, _, name = GetPlayerInfoByGUID(guid);
+ print(guid, name, "send inspect");
+ NotifyInspect(id);
+ CTimerAfter(20, func); -- precaution for never receiving INSPECT_READY
+ end
+end
+
+local encounter = nil;
+local function tick()
+ if not encounter and IsEncounterInProgress() then
+ encounter = true;
+ elseif encounter and not IsEncounterInProgress() then
+ encounter = nil;
+ -- for some reason can't hook to ENCOUNTER_END event so have to track with timer
+ -- TODO support Argus, encounterID 1866
+ for guid, t in pairs(runningcds) do
+ for spellid, _ in pairs(t) do
+ CdUp(guid, spellid);
+ end
+ end
+ end
+ local current = GetTime();
+ for guid, t in pairs(runningcds) do
+ for spellid, start in pairs(t) do
+ local remain = cds[guid][spellid] - (current - start);
+ if remain <= 0 then -- CD back up
+ CdUp(guid, spellid);
+ else -- still on CD
+ if remain >= 60 then
+ frames[guid][spellid].text:SetFormattedText("%d:%02d", floor(remain/60), remain%60);
+ else
+ frames[guid][spellid].text:SetText(floor(remain));
+ end
+ end
+ end
+ end
+ CTimerAfter(0.5, tick);
+end
+
+local function refreshPositions()
+ for i, frame in ipairs(shown) do
+ local prev = shown[i-1] or cdframe;
+ frame:ClearAllPoints();
+ frame:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -1);
+ end
+end
+
+local function removeFrame(frame)
+ unused[#unused+1] = frame;
+ frames[frame.guid][frame.spellid] = nil;
+ cds[frame.guid][frame.spellid] = nil;
+ if runningcds[frame.guid] then runningcds[frame.guid][frame.spellid] = nil end
+ tremove(shown, frame.pos);
+ for i, frame in ipairs(shown) do
+ frame.pos = i;
+ end
+ frame:ClearAllPoints();
+ frame:Hide();
+ refreshPositions();
+end
+
+local unique = 1;
+local function getFrame()
+ local frame = tremove(unused);
+ if not frame then
+ frame = CreateFrame("Frame", format("OmaCD%i", unique), cdframe);
+ unique = unique + 1;
+ frame:SetWidth(75);
+ frame:SetHeight(16);
+ frame.base = frame:CreateTexture(nil, "BACKGROUND");
+ frame.base:SetAllPoints();
+ frame.base:SetColorTexture(1, 1, 1);
+ frame.icon = frame:CreateTexture(nil, "ARTWORK");
+ frame.icon:SetPoint("TOPLEFT");
+ frame.icon:SetPoint("BOTTOMLEFT");
+ frame.icon:SetWidth(16);
+ frame.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93);
+ frame.text = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall");
+ frame.text:SetPoint("LEFT", frame, "LEFT", 20, 0);
+ frame.text:SetJustifyH("LEFT");
+ end
+ return frame;
+end
+local function updateCD(guid, specid, spellid)
+ -- update cd value
+ local cd = trackedcds[specid][spellid];
+ if cdfixes[guid] then cd = cd + cdfixes[guid] end
+ if not cds[guid] then cds[guid] = {} end
+ cds[guid][spellid] = cd;
+ if frames[guid] and frames[guid][spellid] then return end
+
+ -- no cd frame yet, create one
+ local frame = getFrame();
+ frame.name = ssub(UnitName(guidToId[guid]), 1, 8);
+ frame.guid = guid;
+ frame.spellid = spellid;
+ frame.base:SetVertexColor(unpack(upColor));
+ frame.icon:SetTexture(GetSpellTexture(spellid));
+ frame.text:SetText(frame.name);
+ frame:Show();
+ if not frames[guid] then frames[guid] = {} end
+ frames[guid][spellid] = frame;
+ frame.pos = #shown+1;
+ local prev = shown[#shown] or cdframe;
+ shown[#shown+1] = frame;
+ frame:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -1);
+ return frame;
+end
+
+local function updateUnitCD(guid)
+ local specid = guidToSpecid[guid];
+ if specid then
+ for spellid, _ in pairs(trackedcds[specid]) do
+ updateCD(guid, specid, spellid);
+ end
+ end
+end
+
+local function updateMonk(guid)
+ local id = guidToId[guid];
+ if not id then return end -- called because another addon inspected
+ local weapon = GetInventoryItemLink(id, INVSLOT_MAINHAND);
+ if not weapon then
+ -- try again directly
+ if CanInspect(id) then
+ print(guid, "send inspect");NotifyInspect(id);
+ CTimerAfter(20, updateMonk);
+ end
+ return;
+ end
+ if inspectSent[guid] then inspectSent[guid] = nil end
+ print("Got monk", guid);
+ local _, _, _, relic1, relic2, relic3 = strsplit(":", weapon);
+ local cdfix = 0;
+ if monkRelics[relic1] then cdfix = cdfix + monkRelics[relic1] end
+ if monkRelics[relic2] then cdfix = cdfix + monkRelics[relic2] end
+ if monkRelics[relic3] then cdfix = cdfix + monkRelics[relic3] end
+ if cdfix ~= 0 then cdfixes[guid] = cdfix end
+
+ local wrist = GetInventoryItemID(guidToId[guid], INVSLOT_WRIST);
+ if wrist == 137096 then -- Petrichor Lagniappe
+ monksWithLegendary[guid] = true;
+ else
+ monksWithLegendary[guid] = nil;
+ end
+ updateUnitCD(guid);
+end
+
+local function updateDruid(guid, player)
+ local id = guidToId[guid];
+ if id then
+ -- Inner Peace talent
+ local selected;
+ if player then
+ _, _, _, selected = GetTalentInfo(6, 2, 1);
+ else
+ _, _, _, selected = GetTalentInfo(6, 2, 1, true, id);
+ end
+ if selected then
+ cdfixes[guid] = -60;
+ else
+ cdfixes[guid] = nil;
+ end
+ updateUnitCD(guid);
+ end
+end
+
+local function updatePriest(guid)
+ local id = guidToId[guid];
+ if id then
+ local specid = GetInspectSpecialization(id);
+ if specid == 0 then
+ -- try again directly
+ if CanInspect(id) then
+ print(guid, "send inspect");NotifyInspect(id);
+ CTimerAfter(20, updatePriest);
+ end
+ end
+ if inspectSent[guid] then inspectSent[guid] = nil end
+ print("Got priest", guid);
+ guidToSpecid[guid] = specid;
+ updateUnitCD(guid);
+ end
+end
+
+local function updatePlayer()
+ local specid = GetSpecializationInfo(GetSpecialization() or 1);
+ local guid = UnitGUID("player");
+ if trackedcds[specid] then
+ guidToId[guid] = "player";
+ guidToSpecid[guid] = specid;
+ if specid == 270 then -- Monk
+ updateMonk(guid);
+ elseif specid == 105 then -- Druid
+ updateDruid(guid, true);
+ else
+ updateUnitCD(guid);
+ end
+ idToGuid["player"] = guid;
+ else
+ idToGuid["player"] = nil;
+ for _, frame in pairs(frames[guid]) do
+ removeFrame(frame);
+ end
+ end
+end
+
+local function updateUnitid(id)
+ local guid = UnitGUID(id);
+ if UnitIsUnit(id, "player") then -- player is special case
+ return updatePlayer();
+ elseif UnitExists(id) and UnitGroupRolesAssigned(id) == "HEALER" then
+ local _, class = UnitClass(id);
+ if specs[class] ~= nil then
+ guidToId[guid] = id;
+ if specs[class] == false then
+ -- Priest, have to inspect to get spec, try immediately, if not available, inspect
+ local specid = GetInspectSpecialization(id);
+ if specid == 0 then
+ sendInspect(guid, id, updatePriest);
+ elseif specid == 256 or specid == 257 then
+ guidToSpecid[guid] = specid; -- only Holy and Discipline
+ updateUnitCD(guid);
+ end
+ elseif specs[class] == 270 then
+ -- Monk, have to check inventory for CD modifications
+ guidToSpecid[guid] = specs[class];
+ -- updateUnitCD (without having fixes yet)
+ updateUnitCD(guid);
+ sendInspect(guid, id, updateMonk);
+ elseif specs[class] == 105 then
+ -- Druid, have to inspect to get talents
+ guidToSpecid[guid] = specs[class];
+ -- updateUnitCD (without having fixes yet)
+ updateUnitCD(guid);
+ sendInspect(guid, id, updateDruid);
+ else
+ guidToSpecid[guid] = specs[class];
+ updateUnitCD(guid);
+ end
+ idToGuid[id] = guid;
+ end
+ else
+ idToGuid[id] = nil;
+ if frames[guid] then
+ for _, frame in pairs(frames[guid]) do
+ removeFrame(frame);
+ end
+ end
+ end
+end
+
+local updateQueued = nil;
+local function updateUnitids()
+ local size = 0;
+ local prefix;
+ local newIdToGuid = {};
+ local prevIdToGuid = {};
+ guidToId = {};
+ updateQueued = nil;
+
+ for id, guid in pairs(idToGuid) do
+ prevIdToGuid[id] = guid;
+ end
+
+ if IsInGroup() then
+ if IsInRaid() then
+ size = 40;
+ prefix = "raid";
+ else
+ size = 4;
+ prefix = "party";
+ end
+ end
+
+ updatePlayer();
+ if prevIdToGuid["player"] then prevIdToGuid["player"] = nil end
+ for i = 1,size do
+ local id = format("%s%i", prefix, i);
+ if not UnitIsUnit(id, "player") then
+ updateUnitid(id);
+ end
+ if prevIdToGuid[id] then prevIdToGuid[id] = nil end
+ end
+
+ -- clean up players leaving
+ for id, guid in pairs(prevIdToGuid) do
+ if frames[guid] then
+ for _, frame in pairs(frames[guid]) do
+ removeFrame(frame);
+ end
+ end
+ end
+end
+
+local events = {
+ ["UNIT_HEALTH"] = function(id)
+ local guid = idToGuid[id];
+ if guid and cds[guid] and UnitIsDeadOrGhost(id) then
+ dead[guid] = true;
+ for spellid, _ in pairs(cds[guid]) do
+ CdDown(guid, spellid, nil);
+ end
+ elseif guid and dead[guid] and cds[guid] then
+ dead[guid] = nil;
+ for spellid, _ in pairs(cds[guid]) do
+ CdUp(guid, spellid);
+ end
+ end
+ end,
+ ["UNIT_CONNECTION"] = function(id)
+ local guid = idToGuid[id];
+ if guid and cds[guid] and not UnitIsConnected(id) then
+ disconnected[guid] = true;
+ for spellid, _ in pairs(cds[guid]) do
+ CdDown(guid, spellid, nil);
+ end
+ elseif guid and disconnected[guid] and cds[guid] then
+ disconnected[guid] = nil;
+ for spellid, _ in pairs(cds[guid]) do
+ CdUp(guid, spellid);
+ end
+ end
+ end,
+ ["UNIT_SPELLCAST_SUCCEEDED"] = function(id, _, _, _, spellid)
+ local guid = idToGuid[id];
+ if guid and frames[guid] then
+ local frame = frames[guid][spellid];
+ if frame then
+ CdDown(guid, spellid, GetTime());
+ elseif monksWithLegendary[guid] and spellid == 115151 then
+ -- Renewing Mist with legendary affects Revival
+ if runningcds[guid] and runningcds[guid][115310] then
+ runningcds[guid][115310] = runningcds[guid][115310] - 2;
+ end
+ end
+ end
+ end,
+ ["PLAYER_SPECIALIZATION_CHANGED"] = function(id)
+ updateUnitid(id);
+ end,
+ ["UNIT_INVENTORY_CHANGED"] = function(id)
+ local guid = idToGuid[id];
+ if guid and guidToSpecid[guid] == 270 then
+ print("OmaCD Inventory: Monk inventory update");
+ updateMonk(guid);
+ end
+ end,
+ ["GROUP_ROSTER_UPDATE"] = function()
+ if not updateQueued then
+ updateQueued = true
+ CTimerAfter(2, updateUnitids);
+ end
+ end,
+ ["INSPECT_READY"] = function(guid)
+ local specid = guidToSpecid[guid];
+ if inspectSent[guid] then inspectSent[guid] = nil end
+ if specid == 105 then -- Druid
+ print("OmaCD Inspect: druid,", guid);
+ updateDruid(guid);
+ ClearInspectPlayer();
+ elseif specid == 270 then -- Monk
+ print("OmaCD Inspect: monk,", guid);
+ -- don't clear inspect player, have to track inventory changes
+ elseif specid == false or specid == 256 or specid == 257 then -- Priest
+ print("OmaCD Inspect: priest,", guid);
+ updatePriest(guid);
+ ClearInspectPlayer();
+ end
+ end,
+};
+events["PLAYER_ROLES_ASSIGNED"] = events["GROUP_ROSTER_UPDATE"];
+
+local function cdtracker()
+ cdframe:SetFrameStrata("LOW");
+ cdframe:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 200, -200);
+ cdframe:SetWidth(1);
+ cdframe:SetHeight(1);
+
+ cdframe:UnregisterAllEvents();
+ cdframe:SetScript("OnEvent", function(self, event, ...)
+ events[event](...);
+ end);
+ cdframe:RegisterEvent("UNIT_HEALTH");
+ cdframe:RegisterEvent("UNIT_CONNECTION");
+ cdframe:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED");
+ cdframe:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED");
+ cdframe:RegisterEvent("UNIT_INVENTORY_CHANGED");
+ cdframe:RegisterEvent("GROUP_ROSTER_UPDATE");
+ cdframe:RegisterEvent("INSPECT_READY");
+ cdframe:RegisterEvent("PLAYER_ROLES_ASSIGNED");
+ -- initial update
+ events["GROUP_ROSTER_UPDATE"]();
+ CTimerAfter(0.5, tick);
+end
+
+cdframe:RegisterEvent("PLAYER_LOGIN");
+cdframe:SetScript("OnEvent", function(self, event)
+ if event == "PLAYER_LOGIN" then
+ return cdtracker();
+ end
+end);