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