916a5a0e88ddfa91eff091b9346db330cc400bcc
[wowui.git] / OmaCD / Cooldowns.lua
1 -- Cooldowns.lua
2 local _;
3 local pairs, ipairs, unpack = pairs, ipairs, unpack;
4 local tremove, format, ssub = table.remove, string.format, string.sub;
5 local floor, strsplit = math.floor, strsplit;
6 local GetTime, UnitGUID, UnitClass = GetTime, UnitGUID, UnitClass;
7 local UnitName, GetSpellTexture = UnitName, GetSpellTexture;
8 local IsInGroup, IsInRaid = IsInGroup, IsInRaid;
9 local CreateFrame, CTimerAfter = CreateFrame, C_Timer.After;
10 local UnitIsUnit, IsEncounterInProgress = UnitIsUnit, IsEncounterInProgress;
11 local UnitExists, UnitGroupRolesAssigned = UnitExists, UnitGroupRolesAssigned;
12 local UnitIsDeadOrGhost, UnitIsConnected = UnitIsDeadOrGhost, UnitIsConnected;
13 local GetTalentInfo, GetInspectSpecialization = GetTalentInfo, GetInspectSpecialization;
14 local GetSpecializationInfo, GetSpecialization = GetSpecializationInfo, GetSpecialization;
15 local GetInventoryItemLink, GetInventoryItemID = GetInventoryItemLink, GetInventoryItemID;
16 local CanInspect, NotifyInspect, ClearInspectPlayer = CanInspect, NotifyInspect, ClearInspectPlayer;
17 local INVSLOT_MAINHAND, INVSLOT_WRIST = INVSLOT_MAINHAND, INVSLOT_WRIST;
18
19 local upColor = {0, 0.7, 0.2, 0.7};
20 local downColor = {1, 0, 0, 0.5};
21
22 local cdframe = CreateFrame("Frame", "OmaCD", UIParent);
23 local frames = {};
24 local unused = {};
25 local shown = {}; -- simple array of frames in shown order
26
27 local specs = {
28     ["PALADIN"] = 65,
29     ["PRIEST"] = false, -- priest has 2 healing specs
30     ["SHAMAN"] = 264,
31     ["MONK"] = 270,
32     ["DRUID"] = 105,
33 };
34 local trackedcds = {
35     [65] = { -- Holy Paladin
36         [31821] = 180, -- Aura Mastery
37     },
38     [105] = { -- Resto Druid
39         [740] = 180, -- Tranquility
40     },
41     [256] = { -- Discipline Priest
42         [62618] = 180, -- Power Word: Barrier
43     },
44     [257] = { -- Holy Priest
45         [64843] = 180, -- Divine Hymn
46     },
47     [264] = { -- Resto Shaman
48         [108280] = 180, -- Healing Tide Totem
49         [98008] = 180, -- Spirit Link Totem
50     },
51     [270] = { -- Mistweaver Monk
52         [115310] = (180-40), -- Revival, already lowered by Artifact when lvl 75
53     },
54 };
55 local monkRelics = { -- Tendrils of Revival
56     ["151012"] = -10,
57     ["151010"] = -10,
58     ["147112"] = -10,
59     ["152291"] = -10,
60 };
61
62 local cds = {}; -- CD durations currently available modified by passive cdfixes
63 -- CD fixes active e.g. cdfixes[guid] = -60, no need for spellid as
64 -- only classes with one CD have fixes
65 local cdfixes = {};
66 local runningcds = {}; -- CDs currently active
67
68 local idToGuid = {};
69 local guidToId = {};
70 local guidToSpecid = {};
71 local monksWithLegendary = {};
72 local inspectSent = {};
73 local dead = {};
74 local disconnected = {};
75
76 local function CdUp(guid, spellid)
77     local frame = frames[guid][spellid];
78     if runningcds[guid] then runningcds[guid][spellid] = nil end
79     frame.base:SetVertexColor(unpack(upColor));
80     frame.text:SetText(frame.name);
81 end
82
83 local function CdDown(guid, spellid, start)
84     local frame = frames[guid][spellid];
85     if not runningcds[guid] then runningcds[guid] = {} end
86     runningcds[guid][spellid] = start;
87     frame.base:SetVertexColor(unpack(downColor));
88     if start then frame.text:SetText(cds[guid][spellid]) end
89 end
90
91 local function sendInspect(guid, id, func)
92     if not inspectSent[guid] and CanInspect(id) then
93         inspectSent[guid] = true;
94         local _, _, _, _, _, name = GetPlayerInfoByGUID(guid);
95         print(guid, name, "send inspect");
96         NotifyInspect(id);
97         CTimerAfter(20, func); -- precaution for never receiving INSPECT_READY
98     end
99 end
100
101 local encounter = nil;
102 local function tick()
103     if not encounter and IsEncounterInProgress() then
104         encounter = true;
105     elseif encounter and not IsEncounterInProgress() then
106         encounter = nil;
107         -- for some reason can't hook to ENCOUNTER_END event so have to track with timer
108         -- TODO support Argus, encounterID 1866
109         for guid, t in pairs(runningcds) do
110             for spellid, _ in pairs(t) do
111                 CdUp(guid, spellid);
112             end
113         end
114     end
115     local current = GetTime();
116     for guid, t in pairs(runningcds) do
117         for spellid, start in pairs(t) do
118             local remain = cds[guid][spellid] - (current - start);
119             if remain <= 0 then -- CD back up
120                 CdUp(guid, spellid);
121             else -- still on CD
122                 if remain >= 60 then
123                     frames[guid][spellid].text:SetFormattedText("%d:%02d", floor(remain/60), remain%60);
124                 else
125                     frames[guid][spellid].text:SetText(floor(remain));
126                 end
127             end
128         end
129     end
130     CTimerAfter(0.5, tick);
131 end
132
133 local function refreshPositions()
134     for i, frame in ipairs(shown) do
135         local prev = shown[i-1] or cdframe;
136         frame:ClearAllPoints();
137         frame:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -1);
138     end
139 end
140
141 local function removeFrame(frame)
142     unused[#unused+1] = frame;
143     frames[frame.guid][frame.spellid] = nil;
144     cds[frame.guid][frame.spellid] = nil;
145     if runningcds[frame.guid] then runningcds[frame.guid][frame.spellid] = nil end
146     tremove(shown, frame.pos);
147     for i, frame in ipairs(shown) do
148         frame.pos = i;
149     end
150     frame:ClearAllPoints();
151     frame:Hide();
152     refreshPositions();
153 end
154
155 local unique = 1;
156 local function getFrame()
157     local frame = tremove(unused);
158     if not frame then
159         frame = CreateFrame("Frame", format("OmaCD%i", unique), cdframe);
160         unique = unique + 1;
161         frame:SetWidth(75);
162         frame:SetHeight(16);
163         frame.base = frame:CreateTexture(nil, "BACKGROUND");
164         frame.base:SetAllPoints();
165         frame.base:SetColorTexture(1, 1, 1);
166         frame.icon = frame:CreateTexture(nil, "ARTWORK");
167         frame.icon:SetPoint("TOPLEFT");
168         frame.icon:SetPoint("BOTTOMLEFT");
169         frame.icon:SetWidth(16);
170         frame.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93);
171         frame.text = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall");
172         frame.text:SetPoint("LEFT", frame, "LEFT", 20, 0);
173         frame.text:SetJustifyH("LEFT");
174     end
175     return frame;
176 end
177 local function updateCD(guid, specid, spellid)
178     -- update cd value
179     local cd = trackedcds[specid][spellid];
180     if cdfixes[guid] then cd = cd + cdfixes[guid] end
181     if not cds[guid] then cds[guid] = {} end
182     cds[guid][spellid] = cd;
183     if frames[guid] and frames[guid][spellid] then return end
184
185     -- no cd frame yet, create one
186     local frame = getFrame();
187     frame.name = ssub(UnitName(guidToId[guid]), 1, 8);
188     frame.guid = guid;
189     frame.spellid = spellid;
190     frame.base:SetVertexColor(unpack(upColor));
191     frame.icon:SetTexture(GetSpellTexture(spellid));
192     frame.text:SetText(frame.name);
193     frame:Show();
194     if not frames[guid] then frames[guid] = {} end
195     frames[guid][spellid] = frame;
196     frame.pos = #shown+1;
197     local prev = shown[#shown] or cdframe;
198     shown[#shown+1] = frame;
199     frame:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -1);
200     return frame;
201 end
202
203 local function updateUnitCD(guid)
204     local specid = guidToSpecid[guid];
205     if specid then
206         for spellid, _ in pairs(trackedcds[specid]) do
207             updateCD(guid, specid, spellid);
208         end
209     end
210 end
211
212 local function updateMonk(guid)
213     local id = guidToId[guid];
214     if not id then return end -- called because another addon inspected
215     local weapon = GetInventoryItemLink(id, INVSLOT_MAINHAND);
216     if not weapon then
217         -- try again directly
218         if CanInspect(id) then
219             print(guid, "send inspect");NotifyInspect(id);
220             CTimerAfter(20, updateMonk);
221         end
222         return;
223     end
224     if inspectSent[guid] then inspectSent[guid] = nil end
225     print("Got monk", guid);
226     local _, _, _, relic1, relic2, relic3 = strsplit(":", weapon);
227     local cdfix = 0;
228     if monkRelics[relic1] then cdfix = cdfix + monkRelics[relic1] end
229     if monkRelics[relic2] then cdfix = cdfix + monkRelics[relic2] end
230     if monkRelics[relic3] then cdfix = cdfix + monkRelics[relic3] end
231     if cdfix ~= 0 then cdfixes[guid] = cdfix end
232
233     local wrist = GetInventoryItemID(guidToId[guid], INVSLOT_WRIST);
234     if wrist == 137096 then -- Petrichor Lagniappe
235         monksWithLegendary[guid] = true;
236     else
237         monksWithLegendary[guid] = nil;
238     end
239     updateUnitCD(guid);
240 end
241
242 local function updateDruid(guid, player)
243     local id = guidToId[guid];
244     if id then
245         -- Inner Peace talent
246         local selected;
247         if player then
248             _, _, _, selected = GetTalentInfo(6, 2, 1);
249         else
250             _, _, _, selected = GetTalentInfo(6, 2, 1, true, id);
251         end
252         if selected then
253             cdfixes[guid] = -60;
254         else
255             cdfixes[guid] = nil;
256         end
257         updateUnitCD(guid);
258     end
259 end
260
261 local function updatePriest(guid)
262     local id = guidToId[guid];
263     if id then
264         local specid = GetInspectSpecialization(id);
265         if specid == 0 then
266             -- try again directly
267             if CanInspect(id) then
268                 print(guid, "send inspect");NotifyInspect(id);
269                 CTimerAfter(20, updatePriest);
270             end
271         end
272         if inspectSent[guid] then inspectSent[guid] = nil end
273         print("Got priest", guid);
274         guidToSpecid[guid] = specid;
275         updateUnitCD(guid);
276     end
277 end
278
279 local function updatePlayer()
280     local specid = GetSpecializationInfo(GetSpecialization() or 1);
281     local guid = UnitGUID("player");
282     if trackedcds[specid] then
283         guidToId[guid] = "player";
284         guidToSpecid[guid] = specid;
285         if specid == 270 then -- Monk
286             updateMonk(guid);
287         elseif specid == 105 then -- Druid
288             updateDruid(guid, true);
289         else
290             updateUnitCD(guid);
291         end
292         idToGuid["player"] = guid;
293     else
294         idToGuid["player"] = nil;
295         for _, frame in pairs(frames[guid]) do
296             removeFrame(frame);
297         end
298     end
299 end
300
301 local function updateUnitid(id)
302     local guid = UnitGUID(id);
303     if UnitIsUnit(id, "player") then -- player is special case
304         return updatePlayer();
305     elseif UnitExists(id) and UnitGroupRolesAssigned(id) == "HEALER" then
306         local _, class = UnitClass(id);
307         if specs[class] ~= nil then
308             guidToId[guid] = id;
309             if specs[class] == false then
310                 -- Priest, have to inspect to get spec, try immediately, if not available, inspect
311                 local specid = GetInspectSpecialization(id);
312                 if specid == 0 then
313                     sendInspect(guid, id, updatePriest);
314                 elseif specid == 256 or specid == 257 then
315                     guidToSpecid[guid] = specid; -- only Holy and Discipline
316                     updateUnitCD(guid);
317                 end
318             elseif specs[class] == 270 then
319                 -- Monk, have to check inventory for CD modifications
320                 guidToSpecid[guid] = specs[class];
321                 -- updateUnitCD (without having fixes yet)
322                 updateUnitCD(guid);
323                 sendInspect(guid, id, updateMonk);
324             elseif specs[class] == 105 then
325                 -- Druid, have to inspect to get talents
326                 guidToSpecid[guid] = specs[class];
327                 -- updateUnitCD (without having fixes yet)
328                 updateUnitCD(guid);
329                 sendInspect(guid, id, updateDruid);
330             else
331                 guidToSpecid[guid] = specs[class];
332                 updateUnitCD(guid);
333             end
334             idToGuid[id] = guid;
335         end
336     else
337         idToGuid[id] = nil;
338         if frames[guid] then
339             for _, frame in pairs(frames[guid]) do
340                 removeFrame(frame);
341             end
342         end
343     end
344 end
345
346 local updateQueued = nil;
347 local function updateUnitids()
348     local size = 0;
349     local prefix;
350     local newIdToGuid = {};
351     local prevIdToGuid = {};
352     guidToId = {};
353     updateQueued = nil;
354
355     for id, guid in pairs(idToGuid) do
356         prevIdToGuid[id] = guid;
357     end
358
359     if IsInGroup() then
360         if IsInRaid() then
361             size = 40;
362             prefix = "raid";
363         else
364             size = 4;
365             prefix = "party";
366         end
367     end
368
369     updatePlayer();
370     if prevIdToGuid["player"] then prevIdToGuid["player"] = nil end
371     for i = 1,size do
372         local id = format("%s%i", prefix, i);
373         if not UnitIsUnit(id, "player") then
374             updateUnitid(id);
375         end
376         if prevIdToGuid[id] then prevIdToGuid[id] = nil end
377     end
378
379     -- clean up players leaving
380     for id, guid in pairs(prevIdToGuid) do
381         if frames[guid] then
382             for _, frame in pairs(frames[guid]) do
383                 removeFrame(frame);
384             end
385         end
386     end
387 end
388
389 local events = {
390     ["UNIT_HEALTH"] = function(id)
391         local guid = idToGuid[id];
392         if guid and cds[guid] and UnitIsDeadOrGhost(id) then
393             dead[guid] = true;
394             for spellid, _ in pairs(cds[guid]) do
395                 CdDown(guid, spellid, nil);
396             end
397         elseif guid and dead[guid] and cds[guid] then
398             dead[guid] = nil;
399             for spellid, _ in pairs(cds[guid]) do
400                 CdUp(guid, spellid);
401             end
402         end
403     end,
404     ["UNIT_CONNECTION"] = function(id)
405         local guid = idToGuid[id];
406         if guid and cds[guid] and not UnitIsConnected(id) then
407             disconnected[guid] = true;
408             for spellid, _ in pairs(cds[guid]) do
409                 CdDown(guid, spellid, nil);
410             end
411         elseif guid and disconnected[guid] and cds[guid] then
412             disconnected[guid] = nil;
413             for spellid, _ in pairs(cds[guid]) do
414                 CdUp(guid, spellid);
415             end
416         end
417     end,
418     ["UNIT_SPELLCAST_SUCCEEDED"] = function(id, _, _, _, spellid)
419         local guid = idToGuid[id];
420         if guid and frames[guid] then
421             local frame = frames[guid][spellid];
422             if frame then
423                 CdDown(guid, spellid, GetTime());
424             elseif monksWithLegendary[guid] and spellid == 115151 then
425                 -- Renewing Mist with legendary affects Revival
426                 if runningcds[guid] and runningcds[guid][115310] then
427                     runningcds[guid][115310] = runningcds[guid][115310] - 2;
428                 end
429             end
430         end
431     end,
432     ["PLAYER_SPECIALIZATION_CHANGED"] = function(id)
433         updateUnitid(id);
434     end,
435     ["UNIT_INVENTORY_CHANGED"] = function(id)
436         local guid = idToGuid[id];
437         if guid and guidToSpecid[guid] == 270 then
438             print("OmaCD Inventory: Monk inventory update");
439             updateMonk(guid);
440         end
441     end,
442     ["GROUP_ROSTER_UPDATE"] = function()
443         if not updateQueued then
444             updateQueued = true
445             CTimerAfter(2, updateUnitids);
446         end
447     end,
448     ["INSPECT_READY"] = function(guid)
449         local specid = guidToSpecid[guid];
450         if inspectSent[guid] then inspectSent[guid] = nil end
451         if specid == 105 then -- Druid
452             print("OmaCD Inspect: druid,", guid);
453             updateDruid(guid);
454             ClearInspectPlayer();
455         elseif specid == 270 then -- Monk
456             print("OmaCD Inspect: monk,", guid);
457             -- don't clear inspect player, have to track inventory changes
458         elseif specid == false or specid == 256 or specid == 257 then -- Priest
459             print("OmaCD Inspect: priest,", guid);
460             updatePriest(guid);
461             ClearInspectPlayer();
462         end
463     end,
464 };
465 events["PLAYER_ROLES_ASSIGNED"] = events["GROUP_ROSTER_UPDATE"];
466
467 local function cdtracker()
468     cdframe:SetFrameStrata("LOW");
469     cdframe:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 200, -200);
470     cdframe:SetWidth(1);
471     cdframe:SetHeight(1);
472
473     cdframe:UnregisterAllEvents();
474     cdframe:SetScript("OnEvent", function(self, event, ...)
475         events[event](...);
476     end);
477     cdframe:RegisterEvent("UNIT_HEALTH");
478     cdframe:RegisterEvent("UNIT_CONNECTION");
479     cdframe:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED");
480     cdframe:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED");
481     cdframe:RegisterEvent("UNIT_INVENTORY_CHANGED");
482     cdframe:RegisterEvent("GROUP_ROSTER_UPDATE");
483     cdframe:RegisterEvent("INSPECT_READY");
484     cdframe:RegisterEvent("PLAYER_ROLES_ASSIGNED");
485     -- initial update
486     events["GROUP_ROSTER_UPDATE"]();
487     CTimerAfter(0.5, tick);
488 end
489
490 cdframe:RegisterEvent("PLAYER_LOGIN");
491 cdframe:SetScript("OnEvent", function(self, event)
492     if event == "PLAYER_LOGIN" then
493         return cdtracker();
494     end
495 end);