87236a4 - Refactor OmaCD, create a more robust inspect scheme
[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         for _, frame in pairs(frames[guid]) do
286             removeFrame(frame);
287         end
288     end
289 end
290
291 local function updateUnitid(id)
292     local guid = UnitGUID(id);
293     if UnitIsUnit(id, "player") then -- player is special case
294         return updatePlayer();
295     elseif UnitExists(id) and UnitGroupRolesAssigned(id) == "HEALER" then
296         local _, class = UnitClass(id);
297         if specs[class] ~= nil then
298             guidToId[guid] = id;
299             local specid = guidToSpecid[guid];
300             guidToSpecid[guid] = specs[class];
301             if specs[class] == false then
302                 if specid == 256 or specid == 257 then
303                     guidToSpecid[guid] = specid;
304                 else
305                     guidToSpecid[guid] = 257; -- assume Holy for now to get something visible
306                 end
307                 updateUnitCD(guid);
308                 OmaInspect.Request(guid, id, updatePriest);
309             elseif specs[class] == 270 then
310                 -- Monk, have to check inventory for CD modifications
311                 -- updateUnitCD (without having fixes yet)
312                 updateUnitCD(guid);
313                 OmaInspect.Request(guid, id, updateMonk, true);
314             elseif specs[class] == 105 then
315                 -- Druid, have to inspect to get talents
316                 -- updateUnitCD (without having fixes yet)
317                 updateUnitCD(guid);
318                 OmaInspect.Request(guid, id, updateDruid);
319             else
320                 updateUnitCD(guid);
321             end
322             idToGuid[id] = guid;
323         end
324     else
325         idToGuid[id] = nil;
326         if frames[guid] then
327             for _, frame in pairs(frames[guid]) do
328                 removeFrame(frame);
329             end
330         end
331     end
332 end
333
334 local prevGroupType = "solo";
335 local prevGroupSize = 0;
336 local updateQueued = nil;
337 local function updateUnitids()
338     local size = 0;
339     local prefix = "solo";
340     guidToId = {};
341     updateQueued = nil;
342
343     if IsInGroup() then
344         if IsInRaid() then
345             size = 40;
346             prefix = "raid";
347         else
348             size = 4;
349             prefix = "party";
350         end
351     end
352
353     if prevGroupType ~= prefix then
354         -- clean up players from old group
355         for i = 1,prevGroupSize do
356             local id = format("%s%i", prevGroupType, i);
357             local guid = idToGuid[id];
358             idToGuid[id] = nil;
359             if frames[guid] then
360                 print(guid, "removing frames in overall updateUnitids");
361                 for _, frame in pairs(frames[guid]) do
362                     removeFrame(frame);
363                 end
364             end
365         end
366         prevGroupType = prefix;
367         prevGroupSize = size;
368     end
369
370     updatePlayer();
371     for i = 1,size do
372         local id = format("%s%i", prefix, i);
373         if not UnitIsUnit(id, "player") then -- player is already done
374             updateUnitid(id);
375         end
376     end
377 end
378
379 -- just check removals, don't cause inspects
380 local function fastUpdateUnitids()
381     local size = 0;
382     local prefix = "solo";
383     if IsInGroup() then
384         if IsInRaid() then
385             size = 40;
386             prefix = "raid";
387         else
388             size = 4;
389             prefix = "party";
390         end
391     end
392     for i = 1,size do
393         local id = format("%s%i", prefix, i);
394         if not UnitExists(id) or UnitGroupRolesAssigned(id) ~= "HEALER" then
395             idToGuid[id] = nil;
396             if frames[guid] then
397                 for _, frame in pairs(frames[guid]) do
398                     removeFrame(frame);
399                 end
400             end
401         end
402     end
403 end
404
405 local events = {
406     ["UNIT_HEALTH"] = function(id)
407         local guid = idToGuid[id];
408         if guid and cds[guid] and UnitIsDeadOrGhost(id) then
409             dead[guid] = true;
410             for spellid, _ in pairs(cds[guid]) do
411                 CdDown(guid, spellid, nil);
412             end
413         elseif guid and dead[guid] and cds[guid] then
414             dead[guid] = nil;
415             for spellid, _ in pairs(cds[guid]) do
416                 CdUp(guid, spellid);
417             end
418         end
419     end,
420     ["UNIT_CONNECTION"] = function(id)
421         local guid = idToGuid[id];
422         if guid and cds[guid] and not UnitIsConnected(id) then
423             disconnected[guid] = true;
424             for spellid, _ in pairs(cds[guid]) do
425                 CdDown(guid, spellid, nil);
426             end
427         elseif guid and disconnected[guid] and cds[guid] then
428             disconnected[guid] = nil;
429             for spellid, _ in pairs(cds[guid]) do
430                 CdUp(guid, spellid);
431             end
432         end
433     end,
434     ["UNIT_SPELLCAST_SUCCEEDED"] = function(id, _, _, _, spellid)
435         local guid = idToGuid[id];
436         if guid and frames[guid] then
437             local frame = frames[guid][spellid];
438             if frame then
439                 CdDown(guid, spellid, GetTime());
440             elseif monksWithLegendary[guid] and spellid == 115151 then
441                 -- Renewing Mist with legendary affects Revival
442                 if runningcds[guid] and runningcds[guid][115310] then
443                     runningcds[guid][115310] = runningcds[guid][115310] - 2;
444                 end
445             elseif guidToSpecid[guid] == 257 and spellid == 62618 then
446                 -- assumed Holy priest cast Barrier, set to update to Disc
447                 guidToSpecid[guid] = 256;
448                 for _, frame in pairs(frames[guid]) do
449                     removeFrame(frame);
450                 end
451                 updateUnitCD(guid);
452                 CdDown(guid, spellid, GetTime());
453             end
454         end
455     end,
456     ["PLAYER_SPECIALIZATION_CHANGED"] = function(id)
457         updateUnitid(id);
458     end,
459     ["GROUP_ROSTER_UPDATE"] = function()
460         fastUpdateUnitids();
461         if not updateQueued then
462             updateQueued = true
463             CTimerAfter(4, updateUnitids);
464         end
465     end,
466 };
467 events["PLAYER_ROLES_ASSIGNED"] = events["GROUP_ROSTER_UPDATE"];
468 events["PLAYER_ENTERING_WORLD"] = events["GROUP_ROSTER_UPDATE"];
469
470 local function cdtracker()
471     cdframe:SetFrameStrata("LOW");
472     cdframe:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 580, -400);
473     cdframe:SetWidth(1);
474     cdframe:SetHeight(1);
475
476     cdframe:UnregisterAllEvents();
477     cdframe:SetScript("OnEvent", function(self, event, ...)
478         events[event](...);
479     end);
480     cdframe:RegisterEvent("UNIT_HEALTH");
481     cdframe:RegisterEvent("UNIT_CONNECTION");
482     cdframe:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED");
483     cdframe:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED");
484     cdframe:RegisterEvent("GROUP_ROSTER_UPDATE");
485     cdframe:RegisterEvent("PLAYER_ROLES_ASSIGNED");
486     cdframe:RegisterEvent("PLAYER_ENTERING_WORLD");
487     -- initial tick
488     CTimerAfter(0.5, tick);
489     CTimerAfter(10, monkTick);
490 end
491
492 cdframe:RegisterEvent("PLAYER_LOGIN");
493 cdframe:SetScript("OnEvent", function(self, event)
494     if event == "PLAYER_LOGIN" then
495         return cdtracker();
496     end
497 end);