· 5 years ago · Jun 26, 2020, 09:16 PM
1-- *********************************************************
2-- ** Deadly Boss Mods - Core **
3-- ** http://www.deadlybossmods.com **
4-- *********************************************************
5--
6-- This addon is written and copyrighted by:
7-- * Paul Emmerich (Tandanu @ EU-Aegwynn) (DBM-Core)
8-- * Martin Verges (Nitram @ EU-Azshara) (DBM-GUI)
9--
10-- The localizations are written by:
11-- * enGB/enUS: Tandanu http://www.deadlybossmods.com
12-- * deDE: Tandanu http://www.deadlybossmods.com
13-- * zhCN: Diablohu http://wow.gamespot.com.cn
14-- * ruRU: BootWin bootwin@gmail.com
15-- * ruRU: Vampik admin@vampik.ru
16-- * zhTW: Hman herman_c1@hotmail.com
17-- * zhTW: Azael/kc10577 kc10577@hotmail.com
18-- * koKR: BlueNyx bluenyx@gmail.com
19-- * esES: Interplay/1nn7erpLaY http://www.1nn7erpLaY.com
20--
21-- Special thanks to:
22-- * Arta (DBM-Party)
23-- * Omegal @ US-Whisperwind (continuing mod support for 3.2+)
24-- * Tennberg (a lot of fixes in the enGB/enUS localization)
25--
26--
27-- The code of this addon is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 License. (see license.txt)
28-- All included textures and sounds are copyrighted by their respective owners, license information for these media files can be found in the modules that make use of them.
29--
30--
31-- You are free:
32-- * to Share - to copy, distribute, display, and perform the work
33-- * to Remix - to make derivative works
34-- Under the following conditions:
35-- * Attribution. You must attribute the work in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the work). (A link to http://www.deadlybossmods.com is sufficient)
36-- * Noncommercial. You may not use this work for commercial purposes.
37-- * Share Alike. If you alter, transform, or build upon this work, you may distribute the resulting work only under the same or similar license to this one.
38--
39
40
41----------------------
42-- Combat log Fix --
43----------------------
44local tCLFix = 0
45
46local function fCLFix(self,elapsed)
47 tCLFix = tCLFix + elapsed
48 if tCLFix >= 2 then --time (in seconds) it takes before it executes the command on line 6
49 CombatLogClearEntries()
50 tCLFix = 0 --resets the timer
51 end
52end
53
54local f = CreateFrame("frame")
55f:SetScript("OnUpdate", fCLFix)
56
57-------------------------------
58-- Globals/Default Options --
59-------------------------------
60DBM = {
61 Revision = ("$Revision: 4923 $"):sub(12, -3),
62 Version = "4.98",
63 DisplayVersion = "4.98 Warmane-Frostmourne by Celeste", -- the string that is shown as version
64 ReleaseRevision = 4923 -- the revision of the latest stable version that is available (for /dbm ver2)
65}
66
67DBM_SavedOptions = {}
68
69DBM.DefaultOptions = {
70 WarningColors = {
71 {r = 0.41, g = 0.80, b = 0.94}, -- Color 1 - #69CCF0 - Turqoise
72 {r = 0.95, g = 0.95, b = 0.00}, -- Color 2 - #F2F200 - Yellow
73 {r = 1.00, g = 0.50, b = 0.00}, -- Color 3 - #FF8000 - Orange
74 {r = 1.00, g = 0.10, b = 0.10}, -- Color 4 - #FF1A1A - Red
75 },
76 RaidWarningSound = "Sound\\Doodad\\BellTollNightElf.wav",
77 SpecialWarningSound = "Sound\\Spells\\PVPFlagTaken.wav",
78 RaidWarningPosition = {
79 Point = "TOP",
80 X = 0,
81 Y = -185,
82 },
83 StatusEnabled = true,
84 AutoRespond = true,
85 Enabled = true,
86 ShowWarningsInChat = true,
87 ShowFakedRaidWarnings = false,
88 WarningIconLeft = true,
89 WarningIconRight = true,
90 HideBossEmoteFrame = false,
91 SpamBlockRaidWarning = true,
92 SpamBlockBossWhispers = false,
93 ShowMinimapButton = true,
94 FixCLEUOnCombatStart = false,
95 BlockVersionUpdatePopup = true,
96 ShowSpecialWarnings = true,
97 AlwaysShowHealthFrame = false,
98 ShowBigBrotherOnCombatStart = false,
99 RangeFramePoint = "CENTER",
100 RangeFrameX = 50,
101 RangeFrameY = -50,
102 RangeFrameSound1 = "none",
103 RangeFrameSound2 = "none",
104 RangeFrameLocked = false,
105 HPFramePoint = "CENTER",
106 HPFrameX = -50,
107 HPFrameY = 50,
108 HPFrameMaxEntries = 5,
109 SpecialWarningPoint = "CENTER",
110 SpecialWarningX = 0,
111 SpecialWarningY = 75,
112 SpecialWarningFont = STANDARD_TEXT_FONT,
113 SpecialWarningFontSize = 50,
114 SpecialWarningFontColor = {0.0, 0.0, 1.0},
115 HealthFrameGrowUp = false,
116 HealthFrameLocked = false,
117 HealthFrameWidth = 200,
118 ArrowPosX = 0,
119 ArrowPosY = -150,
120 ArrowPoint = "TOP",
121 -- global boss mod settings (overrides mod-specific settings for some options)
122 DontShowBossAnnounces = false,
123 DontSendBossAnnounces = false,
124 DontSendBossWhispers = false,
125 DontSetIcons = false,
126 LatencyThreshold = 250,
127 BigBrotherAnnounceToRaid = false,
128-- HelpMessageShown = false,
129}
130
131DBM.Bars = DBT:New()
132DBM.Mods = {}
133
134------------------------
135-- Global Identifiers --
136------------------------
137DBM_DISABLE_ZONE_DETECTION = newproxy(false)
138DBM_OPTION_SPACER = newproxy(false)
139
140--------------
141-- Locals --
142--------------
143local inCombat = {}
144local combatInfo = {}
145local updateFunctions = {}
146local raid = {}
147local modSyncSpam = {}
148local autoRespondSpam = {}
149local chatPrefix = "<Deadly Boss Mods> "
150local chatPrefixShort = "<DBM> "
151local ver = ("%s (r%d)"):format(DBM.DisplayVersion, DBM.Revision)
152local mainFrame = CreateFrame("Frame")
153local showedUpdateReminder = true
154local combatInitialized = false
155local schedule
156local unschedule
157local loadOptions
158local loadModOptions
159local checkWipe
160local fireEvent
161local wowVersion = select(4, GetBuildInfo())
162
163local enableIcons = true -- set to false when a raid leader or a promoted player has a newer version of DBM
164
165local bannedMods = { -- a list of "banned" (meaning they are replaced by another mod like DBM-Battlegrounds (replaced by DBM-PvP)) boss mods, these mods will not be loaded by DBM (and they wont show up in the GUI)
166 "DBM-Battlegrounds", --replaced by DBM-PvP
167}
168
169--------------------------------------------------------
170-- Cache frequently used global variables in locals --
171--------------------------------------------------------
172local DBM = DBM
173-- these global functions are accessed all the time by the event handler
174-- so caching them is worth the effort
175local ipairs, pairs, next = ipairs, pairs, next
176local tinsert, tremove, twipe = table.insert, table.remove, table.wipe
177local type = type
178local select = select
179local floor = math.floor
180
181-- for Phanx' Class Colors
182local RAID_CLASS_COLORS = CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS
183
184---------------------------------
185-- General (local) functions --
186---------------------------------
187-- checks if a given value is in an array
188-- returns true if it finds the value, false otherwise
189local function checkEntry(t, val)
190 for i, v in ipairs(t) do
191 if v == val then
192 return true
193 end
194 end
195 return false
196end
197
198-- removes all occurrences of a value in an array
199-- returns true if at least one occurrence was remove, false otherwise
200local function removeEntry(t, val)
201 local existed = false
202 for i = #t, 1, -1 do
203 if t[i] == val then
204 table.remove(t, i)
205 existed = true
206 end
207 end
208 return existed
209end
210
211-- automatically sends an addon message to the appropriate channel (BATTLEGROUND, RAID or PARTY)
212local function sendSync(prefix, msg)
213 local zoneType = select(2, IsInInstance())
214 if zoneType == "pvp" or zoneType == "arena" then
215 SendAddonMessage(prefix, msg, "BATTLEGROUND")
216 elseif GetRealNumRaidMembers() > 0 then
217 SendAddonMessage(prefix, msg, "RAID")
218 elseif GetRealNumPartyMembers() > 0 then
219 SendAddonMessage(prefix, msg, "PARTY")
220 end
221end
222
223--
224local function strFromTime(time)
225 if type(time) ~= "number" then time = 0 end
226 time = math.floor(time)
227 if time < 60 then
228 return DBM_CORE_TIMER_FORMAT_SECS:format(time)
229 elseif time % 60 == 0 then
230 return DBM_CORE_TIMER_FORMAT_MINS:format(time/60)
231 else
232 return DBM_CORE_TIMER_FORMAT:format(time/60, time % 60)
233 end
234end
235
236local pformat
237do
238 -- fail-safe format, replaces missing arguments with unknown
239 -- note: doesn't handle cases like %%%s correctly at the moment (should become %unknown, but becomes %%s)
240 -- also, the end of the format directive is not detected in all cases, but handles everything that occurs in our boss mods ;)
241 --> not suitable for general-purpose use, just for our warnings and timers (where an argument like a spell-target might be nil due to missing target information from unreliable detection methods)
242
243 local function replace(cap1, cap2)
244 return cap1 == "%" and DBM_CORE_UNKNOWN
245 end
246
247 function pformat(fstr, ...)
248 local ok, str = pcall(format, fstr, ...)
249 return ok and str or fstr:gsub("(%%+)([^%%%s<]+)", replace):gsub("%%%%", "%%")
250 end
251end
252
253-- sends a whisper to a player by his or her character name or BNet presence id
254-- returns true if the message was sent, nil otherwise
255local function sendWhisper(target, msg)
256 if type(target) == "number" then
257 if not BNIsSelf(target) then -- never send BNet whispers to ourselves
258 BNSendWhisper(target, msg)
259 return true
260 end
261 elseif type(target) == "string" then
262 -- whispering to ourselves here is okay and somewhat useful for whisper-warnings
263 SendChatMessage(msg, "WHISPER", nil, target)
264 return true
265 end
266end
267local BNSendWhisper = sendWhisper
268
269
270--------------
271-- Events --
272--------------
273do
274 local registeredEvents = {}
275 local argsMT = {__index = {}}
276 local args = setmetatable({}, argsMT)
277
278 function argsMT.__index:IsSpellID(a1, a2, a3, a4, a5, a6, a7, a8)
279 local v = self.spellId
280 return v == a1 or v == a2 or v == a3 or v == a4 or v == a5 or v == a6 or v == a7 or v == a8
281 end
282
283 function argsMT.__index:IsPlayer()
284 return bit.band(args.destFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) ~= 0 and bit.band(args.destFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
285 end
286
287 function argsMT.__index:IsPlayerSource()
288 return bit.band(args.sourceFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) ~= 0 and bit.band(args.sourceFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
289 end
290
291 function argsMT.__index:IsPlayerPhased()
292 return not isInPhase(1) and bit.band(args.sourceFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) ~= 1
293 end
294
295 function argsMT.__index:IsPet()
296 return bit.band(args.destFlags, COMBATLOG_OBJECT_TYPE_PET) ~= 0
297 end
298
299 function argsMT.__index:IsPetSource()
300 return bit.band(args.sourceFlags, COMBATLOG_OBJECT_TYPE_PET) ~= 0
301 end
302
303 function argsMT.__index:IsSrcTypePlayer()
304 return bit.band(args.sourceFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
305 end
306
307 function argsMT.__index:IsDestTypePlayer()
308 return bit.band(args.destFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
309 end
310
311 function argsMT.__index:IsSrcTypeHostile()
312 return bit.band(args.sourceFlags, COMBATLOG_OBJECT_REACTION_HOSTILE) ~= 0
313 end
314
315 function argsMT.__index:IsDestTypeHostile()
316 return bit.band(args.destFlags, COMBATLOG_OBJECT_REACTION_HOSTILE) ~= 0
317 end
318
319 function argsMT.__index:GetSrcCreatureID()
320 return tonumber(self.sourceGUID:sub(9, 12), 16) or 0
321 end
322
323 function argsMT.__index:GetDestCreatureID()
324 return tonumber(self.destGUID:sub(9, 12), 16) or 0
325 end
326
327 local function handleEvent(self, event, ...)
328 if not registeredEvents[event] or DBM.Options and not DBM.Options.Enabled then return end
329 for i, v in ipairs(registeredEvents[event]) do
330 if type(v[event]) == "function" and (not v.zones or checkEntry(v.zones, GetRealZoneText()) or checkEntry(v.zones, GetCurrentMapAreaID())) and (not v.Options or v.Options.Enabled) then
331 v[event](v, ...)
332 end
333 end
334 end
335
336 function DBM:RegisterEvents(...)
337 for i = 1, select("#", ...) do
338 local ev = select(i, ...)
339 registeredEvents[ev] = registeredEvents[ev] or {}
340 tinsert(registeredEvents[ev], self)
341 mainFrame:RegisterEvent(ev)
342 end
343 end
344
345 function DBM:UnregisterAllEvents()
346 for i, v in pairs(registeredEvents) do
347 for i = #v, 1 do
348 if v[i] == self then
349 tremove(v, i)
350 end
351 end
352 if #v == 0 then
353 registeredEvents[i] = nil
354 mainFrame:UnregisterEvent(i)
355 end
356 end
357 end
358
359 DBM:RegisterEvents("ADDON_LOADED")
360
361 function DBM:FilterRaidBossEmote(msg, ...)
362 return handleEvent(nil, "CHAT_MSG_RAID_BOSS_EMOTE_FILTERED", msg:gsub("\124c%x+(.-)\124r", "%1"), ...)
363 end
364
365 function DBM:COMBAT_LOG_EVENT_UNFILTERED(timestamp, event, sourceGUID, sourceName, sourceFlags, destGUID, destName, destFlags, ...)
366 if not registeredEvents[event] then return end
367 twipe(args)
368 args.timestamp = timestamp
369 args.event = event
370 args.sourceGUID = sourceGUID
371 args.sourceName = sourceName
372 args.sourceFlags = sourceFlags
373 args.destGUID = destGUID
374 args.destName = destName
375 args.destFlags = destFlags
376 -- taken from Blizzard_CombatLog.lua
377 if event == "SWING_DAMAGE" then
378 args.amount, args.overkill, args.school, args.resisted, args.blocked, args.absorbed, args.critical, args.glancing, args.crushing = select(1, ...)
379 elseif event == "SWING_MISSED" then
380 args.spellName = ACTION_SWING
381 args.missType = select(1, ...)
382 elseif event:sub(1, 5) == "RANGE" then
383 args.spellId, args.spellName, args.spellSchool = select(1, ...)
384 if event == "RANGE_DAMAGE" then
385 args.amount, args.overkill, args.school, args.resisted, args.blocked, args.absorbed, args.critical, args.glancing, args.crushing = select(4, ...)
386 elseif event == "RANGE_MISSED" then
387 args.missType = select(4, ...)
388 end
389 elseif event:sub(1, 5) == "SPELL" then
390 args.spellId, args.spellName, args.spellSchool = select(1, ...)
391 if event == "SPELL_DAMAGE" then
392 args.amount, args.overkill, args.school, args.resisted, args.blocked, args.absorbed, args.critical, args.glancing, args.crushing = select(4, ...)
393 elseif event == "SPELL_MISSED" then
394 args.missType, args.amountMissed = select(4, ...)
395 elseif event == "SPELL_HEAL" then
396 args.amount, args.overheal, args.absorbed, args.critical = select(4, ...)
397 args.school = args.spellSchool
398 elseif event == "SPELL_ENERGIZE" then
399 args.valueType = 2
400 args.amount, args.powerType = select(4, ...)
401 elseif event:sub(1, 14) == "SPELL_PERIODIC" then
402 if event == "SPELL_PERIODIC_MISSED" then
403 args.missType = select(4, ...)
404 elseif event == "SPELL_PERIODIC_DAMAGE" then
405 args.amount, args.overkill, args.school, args.resisted, args.blocked, args.absorbed, args.critical, args.glancing, args.crushing = select(4, ...)
406 elseif event == "SPELL_PERIODIC_HEAL" then
407 args.amount, args.overheal, args.absorbed, args.critical = select(4, ...)
408 args.school = args.spellSchool
409 elseif event == "SPELL_PERIODIC_DRAIN" then
410 args.amount, args.powerType, args.extraAmount = select(4, ...)
411 args.valueType = 2
412 elseif event == "SPELL_PERIODIC_LEECH" then
413 args.amount, args.powerType, args.extraAmount = select(4, ...)
414 args.valueType = 2
415 elseif event == "SPELL_PERIODIC_ENERGIZE" then
416 args.amount, args.powerType = select(4, ...)
417 args.valueType = 2
418 end
419 elseif event == "SPELL_DRAIN" then
420 args.amount, args.powerType, args.extraAmount = select(4, ...)
421 args.valueType = 2
422 elseif event == "SPELL_LEECH" then
423 args.amount, args.powerType, args.extraAmount = select(4, ...)
424 args.valueType = 2
425 elseif event == "SPELL_INTERRUPT" then
426 args.extraSpellId, args.extraSpellName, args.extraSpellSchool = select(4, ...)
427 elseif event == "SPELL_EXTRA_ATTACKS" then
428 args.amount = select(4, ...)
429 elseif event == "SPELL_DISPEL_FAILED" then
430 args.extraSpellId, args.extraSpellName, args.extraSpellSchool = select(4, ...)
431 elseif event == "SPELL_AURA_DISPELLED" then
432 args.extraSpellId, args.extraSpellName, args.extraSpellSchool = select(4, ...)
433 args.auraType = select(7, ...)
434 elseif event == "SPELL_AURA_STOLEN" then
435 args.extraSpellId, args.extraSpellName, args.extraSpellSchool = select(4, ...)
436 args.auraType = select(7, ...)
437 elseif event == "SPELL_AURA_APPLIED" or event == "SPELL_AURA_REMOVED" then
438 args.auraType = select(4, ...)
439 args.sourceName = args.destName
440 args.sourceGUID = args.destGUID
441 args.sourceFlags = args.destFlags
442 elseif event == "SPELL_AURA_APPLIED_DOSE" or event == "SPELL_AURA_REMOVED_DOSE" then
443 args.auraType, args.amount = select(4, ...)
444 args.sourceName = args.destName
445 args.sourceGUID = args.destGUID
446 args.sourceFlags = args.destFlags
447 elseif event == "SPELL_CAST_FAILED" then
448 args.missType = select(4, ...)
449 end
450 elseif event == "DAMAGE_SHIELD" then
451 args.spellId, args.spellName, args.spellSchool = select(1, ...)
452 args.amount, args.school, args.resisted, args.blocked, args.absorbed, args.critical, args.glancing, args.crushing = select(4, ...)
453 elseif event == "DAMAGE_SHIELD_MISSED" then
454 args.spellId, args.spellName, args.spellSchool = select(1, ...)
455 args.missType = select(4, ...)
456 elseif event == "ENCHANT_APPLIED" then
457 args.spellName = select(1,...)
458 args.itemId, args.itemName = select(2,...)
459 elseif event == "ENCHANT_REMOVED" then
460 args.spellName = select(1,...)
461 args.itemId, args.itemName = select(2,...)
462 elseif event == "UNIT_DIED" or event == "UNIT_DESTROYED" then
463 args.sourceName = args.destName
464 args.sourceGUID = args.destGUID
465 args.sourceFlags = args.destFlags
466 elseif event == "ENVIRONMENTAL_DAMAGE" then
467 args.environmentalType = select(1,...)
468 args.amount, args.overkill, args.school, args.resisted, args.blocked, args.absorbed, args.critical, args.glancing, args.crushing = select(2, ...)
469 args.spellName = _G["ACTION_"..event.."_"..args.environmentalType]
470 args.spellSchool = args.school
471 elseif event == "DAMAGE_SPLIT" then
472 args.spellId, args.spellName, args.spellSchool = select(1, ...)
473 args.amount, args.school, args.resisted, args.blocked, args.absorbed, args.critical, args.glancing, args.crushing = select(4, ...)
474 end
475 return handleEvent(nil, event, args)
476 end
477 mainFrame:SetScript("OnEvent", handleEvent)
478end
479
480
481-----------------
482-- Callbacks --
483-----------------
484do
485 local callbacks = {}
486
487 function fireEvent(event, ...)
488 if not callbacks[event] then return end
489 for i, v in ipairs(callbacks[event]) do
490 local ok, err = pcall(v, event, ...)
491 if not ok then DBM:AddMsg(("Error while executing callback %s for event %s: %s"):format(tostring(v), tostring(event), err)) end
492 end
493 end
494
495 function DBM:RegisterCallback(event, f)
496 if not event or type(f) ~= "function" then
497 error("Usage: DBM:RegisterCallback(event, callbackFunc)", 2)
498 end
499 callbacks[event] = callbacks[event] or {}
500 table.insert(callbacks[event], f)
501 return #callbacks[event]
502 end
503end
504
505--------------------------
506-- OnUpdate/Scheduler --
507--------------------------
508do
509 -- stack that stores a few tables (up to 8) which will be recycled
510 local popCachedTable, pushCachedTable
511 local numChachedTables = 0
512 do
513 local tableCache = nil
514
515 -- gets a table from the stack, it will then be recycled.
516 function popCachedTable()
517 local t = tableCache
518 if t then
519 tableCache = t.next
520 numChachedTables = numChachedTables - 1
521 end
522 return t
523 end
524
525 -- tries to push a table on the stack
526 -- only tables with <= 4 array entries are accepted as cached tables are only used for tasks with few arguments for performance reasons
527 -- also, the maximum number of cached tables is limited to 8 as DBM rarely has more than eight scheduled tasks with less than 4 arguments at the same time
528 -- this is just to re-use all the tables of the small tasks that are scheduled all the time (like the wipe detection)
529 -- note that the cache does not use weak references anywhere for performance reasons, so a cached table will never be deleted by the garbage collector
530 function pushCachedTable(t)
531 if numChachedTables < 8 and #t <= 4 then
532 twipe(t)
533 t.next = tableCache
534 tableCache = t
535 numChachedTables = numChachedTables + 1
536 end
537 end
538 end
539
540 -- priority queue (min-heap) that stores all scheduled tasks.
541 -- insert: O(log n)
542 -- deleteMin: O(log n)
543 -- getMin: O(1)
544 -- removeAllMatching: O(n)
545 local insert, removeAllMatching, getMin, deleteMin
546 do
547 local heap = {}
548 local firstFree = 1
549
550 -- gets the next task
551 function getMin()
552 return heap[1]
553 end
554
555 -- restores the heap invariant by moving an item up
556 local function siftUp(n)
557 local parent = floor(n / 2)
558 while n > 1 and heap[parent].time > heap[n].time do -- move the element up until the heap invariant is restored, meaning the element is at the top or the element's parent is <= the element
559 heap[n], heap[parent] = heap[parent], heap[n] -- swap the element with its parent
560 n = parent
561 parent = floor(n / 2)
562 end
563 end
564
565 -- restores the heap invariant by moving an item down
566 local function siftDown(n)
567 local m -- position of the smaller child
568 while 2 * n < firstFree do -- #children >= 1
569 -- swap the element with its smaller child
570 if 2 * n + 1 == firstFree then -- n does not have a right child --> it only has a left child as #children >= 1
571 m = 2 * n -- left child
572 elseif heap[2 * n].time < heap[2 * n + 1].time then -- #children = 2 and left child < right child
573 m = 2 * n -- left child
574 else -- #children = 2 and right child is smaller than the left one
575 m = 2 * n + 1 -- right
576 end
577 if heap[n].time <= heap[m].time then -- n is <= its smallest child --> heap invariant restored
578 return
579 end
580 heap[n], heap[m] = heap[m], heap[n]
581 n = m
582 end
583 end
584
585 -- inserts a new element into the heap
586 function insert(ele)
587 heap[firstFree] = ele
588 siftUp(firstFree)
589 firstFree = firstFree + 1
590 end
591
592 -- deletes the min element
593 function deleteMin()
594 local min = heap[1]
595 firstFree = firstFree - 1
596 heap[1] = heap[firstFree]
597 heap[firstFree] = nil
598 siftDown(1)
599 return min
600 end
601
602 -- removes multiple scheduled tasks from the heap
603 -- note that this function is comparatively slow by design as it has to check all tasks and allows partial matches
604 function removeAllMatching(f, mod, ...)
605 -- remove all elements that match the signature, this destroyes the heap and leaves a normal array
606 local v, match
607 for i = #heap, 1, -1 do -- iterate backwards over the array to allow usage of table.remove
608 v = heap[i]
609 if (not f or v.func == f) and (not mod or v.mod == mod) then
610 match = true
611 for i = 1, select("#", ...) do
612 if select(i, ...) ~= v[i] then
613 match = false
614 break
615 end
616 end
617 if match then
618 table.remove(heap, i)
619 firstFree = firstFree - 1
620 end
621 end
622 end
623 -- rebuild the heap from the array in O(n)
624 for i = floor((firstFree - 1) / 2), 1, -1 do
625 siftDown(i)
626 end
627 end
628 end
629
630 mainFrame:SetScript("OnUpdate", function(self, elapsed)
631 local time = GetTime()
632
633 -- execute scheduled tasks
634 local nextTask = getMin()
635 while nextTask and nextTask.time <= time do
636 deleteMin()
637 nextTask.func(unpack(nextTask))
638 pushCachedTable(nextTask)
639 nextTask = getMin()
640 end
641
642 -- execute OnUpdate handlers of all modules
643 for i, v in pairs(updateFunctions) do
644 if i.Options.Enabled and (not i.zones or checkEntry(i.zones, GetRealZoneText()) or checkEntry(i.zones, GetCurrentMapAreaID())) then
645 i.elapsed = (i.elapsed or 0) + elapsed
646 if i.elapsed >= (i.updateInterval or 0) then
647 v(i, i.elapsed)
648 i.elapsed = 0
649 end
650 end
651 end
652
653 -- clean up sync spam timers and auto respond spam blockers
654 -- TODO: optimize this; using next(t, k) all the time on nearly empty hash tables is not a good idea...doesn't really matter here as modSyncSpam only very rarely contains more than 4 entries...
655 local k, v = next(modSyncSpam, nil)
656 if v and (time - v > 2.5) then
657 modSyncSpam[k] = nil
658 end
659 end)
660
661 function schedule(t, f, mod, ...)
662 local v
663 if numChachedTables > 0 and select("#", ...) <= 4 then -- a cached table is available and all arguments fit into an array with four slots
664 v = popCachedTable()
665 v.time = GetTime() + t
666 v.func = f
667 v.mod = mod
668 for i = 1, select("#", ...) do
669 v[i] = select(i, ...)
670 end
671 else -- create a new table
672 v = {time = GetTime() + t, func = f, mod = mod, ...}
673 end
674 insert(v)
675 end
676
677 function unschedule(f, mod, ...)
678 return removeAllMatching(f, mod, ...)
679 end
680end
681
682function DBM:Schedule(t, f, ...)
683 return schedule(t, f, nil, ...)
684end
685
686function DBM:Unschedule(f, ...)
687 return unschedule(f, nil, ...)
688end
689
690function DBM:ForceUpdate()
691 mainFrame:GetScript("OnUpdate")(mainFrame, 0)
692end
693
694----------------------
695-- Slash Commands --
696----------------------
697SLASH_DEADLYBOSSMODS1 = "/dbm"
698SlashCmdList["DEADLYBOSSMODS"] = function(msg)
699 local cmd = msg:lower()
700 if cmd == "ver" or cmd == "version" then
701 DBM:ShowVersions(false)
702 elseif cmd == "ver2" or cmd == "version2" then
703 DBM:ShowVersions(true)
704 elseif cmd == "rel" or cmd == "release" then -- Added to broadcast the version check to raid chat
705 DBM:ShowRelease(true)
706 elseif cmd == "unlock" or cmd == "move" then
707 DBM.Bars:ShowMovableBar()
708 elseif cmd == "help" then
709 for i, v in ipairs(DBM_CORE_SLASHCMD_HELP) do DBM:AddMsg(v) end
710 elseif cmd:sub(1, 5) == "timer" then
711 local time, text = msg:match("^%w+ ([%d:]+) (.+)$")
712 if not (time and text) then
713 DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
714 return
715 end
716 local min, sec = string.split(":", time)
717 min = tonumber(min or "") or 0
718 sec = tonumber(sec or "")
719 if min and not sec then
720 sec = min
721 min = 0
722 end
723 time = min * 60 + sec
724 DBM:CreatePizzaTimer(time, text)
725 elseif cmd:sub(1, 15) == "broadcast timer" then
726 local time, text = msg:match("^%w+ %w+ ([%d:]+) (.+)$")
727 if DBM:GetRaidRank() == 0 then
728 DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
729 end
730 if not (time and text) then
731 DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
732 return
733 end
734 local min, sec = string.split(":", time)
735 min = tonumber(min or "") or 0
736 sec = tonumber(sec or "")
737 if min and not sec then
738 sec = min
739 min = 0
740 end
741 time = min * 60 + sec
742 DBM:CreatePizzaTimer(time, text, true)
743 elseif cmd:sub(0,5) == "break" then
744 if DBM:GetRaidRank() == 0 then
745 DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
746 return
747 end
748 local timer = tonumber(cmd:sub(6)) or 5
749 local timer = timer * 60
750 local channel = ((GetNumRaidMembers() == 0) and "PARTY") or "RAID_WARNING"
751 DBM:CreatePizzaTimer(timer, DBM_CORE_TIMER_BREAK, true)
752 DBM:Unschedule(SendChatMessage)
753 SendChatMessage(DBM_CORE_BREAK_START:format(timer/60), channel)
754 if timer/60 > 5 then DBM:Schedule(timer - 5*60, SendChatMessage, DBM_CORE_BREAK_MIN:format(5), channel) end
755 if timer/60 > 2 then DBM:Schedule(timer - 2*60, SendChatMessage, DBM_CORE_BREAK_MIN:format(2), channel) end
756 if timer/60 > 1 then DBM:Schedule(timer - 1*60, SendChatMessage, DBM_CORE_BREAK_MIN:format(1), channel) end
757 if timer > 30 then DBM:Schedule(timer - 30, SendChatMessage, DBM_CORE_BREAK_SEC:format(30), channel) end
758 DBM:Schedule(timer, SendChatMessage, DBM_CORE_ANNOUNCE_BREAK_OVER, channel)
759 elseif cmd:sub(1, 4) == "pull" then
760 if DBM:GetRaidRank() == 0 then
761 return DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
762 end
763 local timer = tonumber(cmd:sub(5)) or 10
764 local channel = ((GetNumRaidMembers() == 0) and "PARTY") or "RAID_WARNING"
765 DBM:CreatePizzaTimer(timer, DBM_CORE_TIMER_PULL, true)
766 SendChatMessage(DBM_CORE_ANNOUNCE_PULL:format(timer), channel)
767 if timer > 7 then DBM:Schedule(timer - 7, SendChatMessage, DBM_CORE_ANNOUNCE_PULL:format(7), channel) end
768 if timer > 5 then DBM:Schedule(timer - 5, SendChatMessage, DBM_CORE_ANNOUNCE_PULL:format(5), channel) end
769 if timer > 3 then DBM:Schedule(timer - 3, SendChatMessage, DBM_CORE_ANNOUNCE_PULL:format(3), channel) end
770 if timer > 2 then DBM:Schedule(timer - 2, SendChatMessage, DBM_CORE_ANNOUNCE_PULL:format(2), channel) end
771 if timer > 1 then DBM:Schedule(timer - 1, SendChatMessage, DBM_CORE_ANNOUNCE_PULL:format(1), channel) end
772 DBM:Schedule(timer, SendChatMessage, DBM_CORE_ANNOUNCE_PULL_NOW, channel)
773 elseif cmd:sub(1, 5) == "arrow" then
774 if not DBM:IsInRaid() then
775 DBM:AddMsg(DBM_ARROW_NO_RAIDGROUP)
776 return false
777 end
778 local x, y = string.split(" ", cmd:sub(6):trim())
779 xNum, yNum = tonumber(x or ""), tonumber(y or "")
780 local success
781 if xNum and yNum then
782 DBM.Arrow:ShowRunTo(xNum / 100, yNum / 100, 0)
783 success = true
784 elseif type(x) == "string" and x:trim() ~= "" then
785 local subCmd = x:trim()
786 if subCmd:upper() == "HIDE" then
787 DBM.Arrow:Hide()
788 success = true
789 elseif subCmd:upper() == "MOVE" then
790 DBM.Arrow:Move()
791 success = true
792 elseif subCmd:upper() == "TARGET" then
793 DBM.Arrow:ShowRunTo("target")
794 success = true
795 elseif subCmd:upper() == "FOCUS" then
796 DBM.Arrow:ShowRunTo("focus")
797 success = true
798 elseif DBM:GetRaidUnitId(DBM:Capitalize(subCmd)) ~= "none" then
799 DBM.Arrow:ShowRunTo(DBM:Capitalize(subCmd))
800 success = true
801 end
802 end
803 if not success then
804 for i, v in ipairs(DBM_ARROW_ERROR_USAGE) do
805 DBM:AddMsg(v)
806 end
807 end
808 else
809 DBM:LoadGUI()
810 end
811end
812
813SLASH_DBMRANGE1 = "/range"
814SLASH_DBMRANGE2 = "/distance"
815SlashCmdList["DBMRANGE"] = function(msg)
816 if DBM.RangeCheck:IsShown() then
817 DBM.RangeCheck:Hide()
818 else
819 local r = tonumber(msg)
820 if r and (r == 10 or r == 11 or r == 15 or r == 28 or r == 12 or r == 6 or r == 8 or r == 20) then
821 DBM.RangeCheck:Show(r)
822 else
823 DBM.RangeCheck:Show(10)
824 end
825 end
826end
827
828do
829 local sortMe = {}
830 local function sort(v1, v2)
831 return (v1.revision or 0) > (v2.revision or 0)
832 end
833 function DBM:ShowVersions(notify)
834 for i, v in pairs(raid) do
835 table.insert(sortMe, v)
836 end
837 table.sort(sortMe, sort)
838 self:AddMsg(DBM_CORE_VERSIONCHECK_HEADER)
839 for i, v in ipairs(sortMe) do
840 if v.displayVersion then
841 self:AddMsg(DBM_CORE_VERSIONCHECK_ENTRY:format(v.name, v.displayVersion, v.revision))
842 if notify and v.displayVersion ~= DBM.Version and v.revision < DBM.ReleaseRevision then
843 SendChatMessage(chatPrefixShort..DBM_CORE_YOUR_VERSION_OUTDATED, "WHISPER", nil, v.name)
844 end
845 else
846 self:AddMsg(DBM_CORE_VERSIONCHECK_ENTRY_NO_DBM:format(v.name))
847 end
848 end
849 for i = #sortMe, 1, -1 do
850 if not sortMe[i].revision then
851 table.remove(sortMe, i)
852 end
853 end
854 self:AddMsg(DBM_CORE_VERSIONCHECK_FOOTER:format(#sortMe))
855 for i = #sortMe, 1, -1 do
856 sortMe[i] = nil
857 end
858 end
859 function DBM:ShowRelease(notify)
860 if DBM:GetRaidRank() == 0 then
861 DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
862 return
863 end
864 for i, v in pairs(raid) do
865 table.insert(sortMe, v)
866 end
867 table.sort(sortMe, sort)
868 SendChatMessage(DBM_CORE_VERSIONCHECK_HEADER, "RAID")
869 for i, v in ipairs(sortMe) do
870 if v.displayVersion then
871 SendChatMessage(DBM_CORE_VERSIONCHECK_ENTRY:format(v.name, v.displayVersion, v.revision), "RAID")
872 --[[if notify and v.displayVersion ~= DBM.Version and v.revision < DBM.ReleaseRevision then
873 SendChatMessage(chatPrefixShort..DBM_CORE_YOUR_VERSION_OUTDATED, "WHISPER", nil, v.name)
874 end]]--
875 else
876 SendChatMessage(DBM_CORE_VERSIONCHECK_ENTRY_NO_DBM:format(v.name), "RAID")
877 end
878 end
879 for i = #sortMe, 1, -1 do
880 if not sortMe[i].revision then
881 table.remove(sortMe, i)
882 end
883 end
884 SendChatMessage(DBM_CORE_VERSIONCHECK_FOOTER:format(#sortMe), "RAID")
885 for i = #sortMe, 1, -1 do
886 sortMe[i] = nil
887 end
888 end
889end
890
891-------------------
892-- Pizza Timer --
893-------------------
894do
895 local ignore = {}
896 function DBM:CreatePizzaTimer(time, text, broadcast, sender)
897 if sender and ignore[sender] then return end
898 text = text:sub(1, 16)
899 text = text:gsub("%%t", UnitName("target") or "<no target>")
900 self.Bars:CreateBar(time, text)
901 if broadcast and self:GetRaidRank() >= 1 then
902 sendSync("DBMv4-Pizza", ("%s\t%s"):format(time, text))
903 end
904 if sender then DBM:ShowPizzaInfo(text, sender) end
905 end
906
907 function DBM:AddToPizzaIgnore(name)
908 ignore[name] = true
909 end
910end
911
912function DBM:ShowPizzaInfo(id, sender)
913 self:AddMsg(DBM_PIZZA_SYNC_INFO:format(sender, id))
914end
915
916
917
918------------------
919-- Hyperlinks --
920------------------
921do
922 local ignore, cancel
923 StaticPopupDialogs["DBM_CONFIRM_IGNORE"] = {
924 text = DBM_PIZZA_CONFIRM_IGNORE,
925 button1 = YES,
926 button2 = NO,
927 OnAccept = function(self)
928 DBM:AddToPizzaIgnore(ignore)
929 DBM.Bars:CancelBar(cancel)
930 end,
931 timeout = 0,
932 hideOnEscape = 1,
933 }
934
935 DEFAULT_CHAT_FRAME:HookScript("OnHyperlinkClick", function(self, link, string, button, ...)
936 local linkType, arg1, arg2, arg3 = strsplit(":", link)
937 if linkType == "DBM" and arg1 == "cancel" then
938 DBM.Bars:CancelBar(link:match("DBM:cancel:(.+):nil$"))
939 elseif linkType == "DBM" and arg1 == "ignore" then
940 cancel = link:match("DBM:ignore:(.+):[^%s:]+$")
941 ignore = link:match(":([^:]+)$")
942 StaticPopup_Show("DBM_CONFIRM_IGNORE", ignore)
943 elseif linkType == "DBM" and arg1 == "update" then
944 DBM:ShowUpdateReminder(arg2, arg3) -- displayVersion, revision
945 end
946 end)
947end
948
949do
950 local old = ItemRefTooltip.SetHyperlink -- we have to hook this function since the default ChatFrame code assumes that all links except for player and channel links are valid arguments for this function
951 function ItemRefTooltip:SetHyperlink(link, ...)
952 if link:match("^DBM") then return end
953 return old(self, link, ...)
954 end
955end
956
957
958-----------------
959-- GUI Stuff --
960-----------------
961do
962 local callOnLoad = {}
963 function DBM:LoadGUI()
964 if not IsAddOnLoaded("DBM-GUI") then
965 local _, _, _, enabled = GetAddOnInfo("DBM-GUI")
966 if not enabled then
967 EnableAddOn("DBM-GUI")
968 end
969 local loaded, reason = LoadAddOn("DBM-GUI")
970 if not loaded then
971 if reason then
972 self:AddMsg(DBM_CORE_LOAD_GUI_ERROR:format(tostring(_G["ADDON_"..reason or ""])))
973 else
974 self:AddMsg(DBM_CORE_LOAD_GUI_ERROR:format(DBM_CORE_UNKNOWN))
975 end
976 return false
977 end
978 table.sort(callOnLoad, function(v1, v2) return v1[2] < v2[2] end)
979 for i, v in ipairs(callOnLoad) do v[1]() end
980 collectgarbage("collect")
981 end
982 return DBM_GUI:ShowHide()
983 end
984
985 function DBM:RegisterOnGuiLoadCallback(f, sort)
986 table.insert(callOnLoad, {f, sort or math.huge})
987 end
988end
989
990
991----------------------
992-- Minimap Button --
993----------------------
994do
995 local dragMode = nil
996
997 local function moveButton(self)
998 if dragMode == "free" then
999 local centerX, centerY = Minimap:GetCenter()
1000 local x, y = GetCursorPosition()
1001 x, y = x / self:GetEffectiveScale() - centerX, y / self:GetEffectiveScale() - centerY
1002 self:ClearAllPoints()
1003 self:SetPoint("CENTER", x, y)
1004 else
1005 local centerX, centerY = Minimap:GetCenter()
1006 local x, y = GetCursorPosition()
1007 x, y = x / self:GetEffectiveScale() - centerX, y / self:GetEffectiveScale() - centerY
1008 centerX, centerY = math.abs(x), math.abs(y)
1009 centerX, centerY = (centerX / math.sqrt(centerX^2 + centerY^2)) * 80, (centerY / sqrt(centerX^2 + centerY^2)) * 80
1010 centerX = x < 0 and -centerX or centerX
1011 centerY = y < 0 and -centerY or centerY
1012 self:ClearAllPoints()
1013 self:SetPoint("CENTER", centerX, centerY)
1014 end
1015 end
1016
1017 local button = CreateFrame("Button", "DBMMinimapButton", Minimap)
1018 button:SetHeight(32)
1019 button:SetWidth(32)
1020 button:SetFrameStrata("MEDIUM")
1021 button:SetPoint("CENTER", -65.35, -38.8)
1022 button:SetMovable(true)
1023 button:SetUserPlaced(true)
1024 button:SetNormalTexture("Interface\\AddOns\\DBM-Core\\textures\\Minimap-Button-Up")
1025 button:SetPushedTexture("Interface\\AddOns\\DBM-Core\\textures\\Minimap-Button-Down")
1026 button:SetHighlightTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight")
1027
1028 button:SetScript("OnMouseDown", function(self, button)
1029 if IsShiftKeyDown() and IsAltKeyDown() then
1030 dragMode = "free"
1031 self:SetScript("OnUpdate", moveButton)
1032 elseif IsShiftKeyDown() or button == "RightButton" then
1033 dragMode = nil
1034 self:SetScript("OnUpdate", moveButton)
1035 end
1036 end)
1037 button:SetScript("OnMouseUp", function(self)
1038 self:SetScript("OnUpdate", nil)
1039 end)
1040 button:SetScript("OnClick", function(self, button)
1041 if IsShiftKeyDown() or button == "RightButton" then return end
1042 DBM:LoadGUI()
1043 end)
1044 button:SetScript("OnEnter", function(self)
1045 GameTooltip_SetDefaultAnchor(GameTooltip, self)
1046 GameTooltip:SetText(DBM_CORE_MINIMAP_TOOLTIP_HEADER, 1, 1, 1)
1047 GameTooltip:AddLine(ver, NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b, 1)
1048 GameTooltip:AddLine(" ")
1049 GameTooltip:AddLine(DBM_CORE_MINIMAP_TOOLTIP_FOOTER, RAID_CLASS_COLORS.MAGE.r, RAID_CLASS_COLORS.MAGE.g, RAID_CLASS_COLORS.MAGE.b, 1)
1050 GameTooltip:Show()
1051 end)
1052 button:SetScript("OnLeave", function(self)
1053 GameTooltip:Hide()
1054 end)
1055
1056 function DBM:ToggleMinimapButton()
1057 self.Options.ShowMinimapButton = not self.Options.ShowMinimapButton
1058 if self.Options.ShowMinimapButton then
1059 button:Show()
1060 else
1061 button:Hide()
1062 end
1063 end
1064
1065 function DBM:HideMinimapButton()
1066 return button:Hide()
1067 end
1068end
1069
1070
1071---------------------------
1072-- Raid/Party Handling --
1073---------------------------
1074do
1075 local inRaid = false
1076 local playerRank = 0
1077
1078
1079 function DBM:RAID_ROSTER_UPDATE()
1080 if GetNumRaidMembers() >= 1 then
1081 if not inRaid then
1082 inRaid = true
1083 sendSync("DBMv4-Ver", "Hi!")
1084 self:Schedule(2, DBM.RequestTimers, DBM)
1085 fireEvent("raidJoin", UnitName("player"))
1086 end
1087 local playerWithHigherVersionPromoted = false
1088 for i = 1, GetNumRaidMembers() do
1089 local name, rank, subgroup, _, _, fileName = GetRaidRosterInfo(i)
1090 if name and inRaid then
1091
1092 if (not raid[name]) then
1093 fireEvent("raidJoin", name)
1094 end
1095
1096 raid[name] = raid[name] or {}
1097 raid[name].name = name
1098 raid[name].rank = rank
1099 raid[name].subgroup = subgroup
1100 raid[name].class = fileName
1101 raid[name].id = "raid"..i
1102 raid[name].updated = true
1103 end
1104 if not playerWithHigherVersionPromoted and rank >= 1 and raid[name].version and raid[name].version > tonumber(DBM.Version) then
1105 playerWithHigherVersionPromoted = true
1106 end
1107 end
1108 enableIcons = not playerWithHigherVersionPromoted
1109 for i, v in pairs(raid) do
1110 if not v.updated then
1111 raid[i] = nil
1112 fireEvent("raidLeave", i)
1113 else
1114 v.updated = nil
1115 end
1116 end
1117 else
1118 inRaid = false
1119 enableIcons = true
1120 fireEvent("raidLeave", UnitName("player"))
1121 end
1122 end
1123
1124
1125 function DBM:PARTY_MEMBERS_CHANGED()
1126 if GetNumRaidMembers() > 0 then return end
1127 if GetNumPartyMembers() >= 1 then
1128 if not inRaid then
1129 inRaid = true
1130 sendSync("DBMv4-Ver", "Hi!")
1131 self:Schedule(2, DBM.RequestTimers, DBM)
1132 fireEvent("partyJoin", UnitName("player"))
1133 end
1134 for i = 0, GetNumPartyMembers() do
1135 local id
1136 if (i == 0) then
1137 id = "player"
1138 else
1139 id = "party"..i
1140 end
1141 local name, server = UnitName(id)
1142 local rank, _, fileName = UnitIsPartyLeader(id), UnitClass(id)
1143 if server and server ~= "" then
1144 name = name.."-"..server
1145 end
1146 if (not raid[name]) and inRaid then
1147 fireEvent("partyJoin", name)
1148 end
1149 raid[name] = raid[name] or {}
1150 raid[name].name = name
1151 if rank then
1152 raid[name].rank = 2
1153 else
1154 raid[name].rank = 0
1155 end
1156 raid[name].class = fileName
1157 raid[name].id = id
1158 raid[name].updated = true
1159 end
1160 for i, v in pairs(raid) do
1161 if not v.updated then
1162 raid[i] = nil
1163 fireEvent("partyLeave", i)
1164 else
1165 v.updated = nil
1166 end
1167 end
1168 else
1169 inRaid = false
1170 enableIcons = true
1171 end
1172 end
1173
1174 function DBM:IsInRaid()
1175 return inRaid
1176 end
1177
1178 function DBM:GetRaidRank(name)
1179 name = name or UnitName("player")
1180 return (raid[name] and raid[name].rank) or 0
1181 end
1182
1183 function DBM:GetRaidSubgroup(name)
1184 name = name or UnitName("player")
1185 return (raid[name] and raid[name].subgroup) or 0
1186 end
1187
1188 function DBM:GetRaidClass(name)
1189 name = name or UnitName("player")
1190 return (raid[name] and raid[name].class) or "UNKNOWN"
1191 end
1192
1193 function DBM:GetRaidUnitId(name)
1194 name = name or UnitName("player")
1195 return (raid[name] and raid[name].id) or "none"
1196 end
1197end
1198
1199
1200---------------
1201-- Options --
1202---------------
1203do
1204 local function addDefaultOptions(t1, t2)
1205 for i, v in pairs(t2) do
1206 if t1[i] == nil then
1207 t1[i] = v
1208 elseif type(v) == "table" then
1209 addDefaultOptions(v, t2[i])
1210 end
1211 end
1212 end
1213
1214 local function setRaidWarningPositon()
1215 RaidWarningFrame:ClearAllPoints()
1216 RaidWarningFrame:SetPoint(DBM.Options.RaidWarningPosition.Point, UIParent, DBM.Options.RaidWarningPosition.Point, DBM.Options.RaidWarningPosition.X, DBM.Options.RaidWarningPosition.Y)
1217 end
1218
1219 function loadOptions()
1220 DBM.Options = DBM_SavedOptions
1221 addDefaultOptions(DBM.Options, DBM.DefaultOptions)
1222 -- load special warning options
1223 DBM:UpdateSpecialWarningOptions()
1224 -- set this with a short delay to prevent issues with other addons also trying to do the same thing with another position ;)
1225 DBM:Schedule(5, setRaidWarningPositon)
1226 end
1227
1228 function loadModOptions(modId)
1229 local savedOptions = _G[modId:gsub("-", "").."_SavedVars"] or {}
1230 local savedStats = _G[modId:gsub("-", "").."_SavedStats"] or {}
1231 for i, v in ipairs(DBM.Mods) do
1232 if v.modId == modId then
1233 savedOptions[v.id] = savedOptions[v.id] or v.Options
1234 for option, optionValue in pairs(v.Options) do
1235 if savedOptions[v.id][option] == nil then
1236 savedOptions[v.id][option] = optionValue
1237 end
1238 end
1239 v.Options = savedOptions[v.id] or {}
1240 savedStats[v.id] = savedStats[v.id] or {}
1241 v.stats = savedStats[v.id]
1242
1243 -- for some reason some people have 0 kills as nil instead of 0.
1244 v.stats.kills = v.stats.kills or 0
1245 v.stats.pulls = v.stats.pulls or 0
1246 v.stats.heroicKills = v.stats.heroicKills or 0
1247 v.stats.heroicPulls = v.stats.heroicPulls or 0
1248
1249 if v.OnInitialize then v:OnInitialize() end
1250 for i, cat in ipairs(v.categorySort) do -- temporary hack
1251 if cat == "misc" then
1252 table.remove(v.categorySort, i)
1253 table.insert(v.categorySort, cat)
1254 break
1255 end
1256 end
1257 end
1258 end
1259 _G[modId:gsub("-", "").."_SavedVars"] = savedOptions
1260 _G[modId:gsub("-", "").."_SavedStats"] = savedStats
1261 end
1262end
1263
1264
1265--------------
1266-- OnLoad --
1267--------------
1268do
1269 local function showOldVerWarning()
1270 StaticPopupDialogs["DBM_OLD_VERSION"] = {
1271 text = DBM_CORE_ERROR_DBMV3_LOADED,
1272 button1 = DBM_CORE_OK,
1273 OnAccept = function()
1274 DisableAddOn("DBM_API")
1275 ReloadUI()
1276 end,
1277 timeout = 0,
1278 exclusive = 1,
1279 whileDead = 1,
1280 }
1281 StaticPopup_Show("DBM_OLD_VERSION")
1282 end
1283
1284 local function setCombatInitialized()
1285 combatInitialized = true
1286 end
1287
1288 function DBM:ADDON_LOADED(modname)
1289 if modname == "DBM-Core" then
1290 loadOptions()
1291 DBM.Bars:LoadOptions("DBM")
1292 DBM.Arrow:LoadPosition()
1293 if not DBM.Options.ShowMinimapButton then DBM:HideMinimapButton() end
1294 self.AddOns = {}
1295 for i = 1, GetNumAddOns() do
1296 if GetAddOnMetadata(i, "X-DBM-Mod") and not checkEntry(bannedMods, GetAddOnInfo(i)) then
1297 table.insert(self.AddOns, {
1298 sort = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Sort") or math.huge) or math.huge,
1299 category = GetAddOnMetadata(i, "X-DBM-Mod-Category") or "Other",
1300 name = GetAddOnMetadata(i, "X-DBM-Mod-Name") or "",
1301 zone = {strsplit(",", GetAddOnMetadata(i, "X-DBM-Mod-LoadZone") or "")},
1302 zoneId = {strsplit(",", GetAddOnMetadata(i, "X-DBM-Mod-LoadZoneID") or "")},
1303 subTabs = GetAddOnMetadata(i, "X-DBM-Mod-SubCategories") and {strsplit(",", GetAddOnMetadata(i, "X-DBM-Mod-SubCategories"))},
1304 hasHeroic = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Has-Heroic-Mode") or 1) == 1,
1305 modId = GetAddOnInfo(i),
1306 })
1307 for k, v in ipairs(self.AddOns[#self.AddOns].zone) do
1308 self.AddOns[#self.AddOns].zone[k] = (self.AddOns[#self.AddOns].zone[k]):trim()
1309 end
1310 for i = #self.AddOns[#self.AddOns].zoneId, 1, -1 do
1311 local id = tonumber(self.AddOns[#self.AddOns].zoneId[i])
1312 if id then
1313 self.AddOns[#self.AddOns].zoneId[i] = id
1314 else
1315 table.remove(self.AddOns[#self.AddOns].zoneId, i)
1316 end
1317 end
1318 if self.AddOns[#self.AddOns].subTabs then
1319 for k, v in ipairs(self.AddOns[#self.AddOns].subTabs) do
1320 self.AddOns[#self.AddOns].subTabs[k] = (self.AddOns[#self.AddOns].subTabs[k]):trim()
1321 end
1322 end
1323 end
1324 end
1325 table.sort(self.AddOns, function(v1, v2) return v1.sort < v2.sort end)
1326 self:RegisterEvents(
1327 "COMBAT_LOG_EVENT_UNFILTERED",
1328 "ZONE_CHANGED_NEW_AREA",
1329 "RAID_ROSTER_UPDATE",
1330 "PARTY_MEMBERS_CHANGED",
1331 "CHAT_MSG_ADDON",
1332 "PLAYER_REGEN_DISABLED",
1333 "UNIT_DIED",
1334 "UNIT_DESTROYED",
1335 "CHAT_MSG_WHISPER",
1336 "CHAT_MSG_BN_WHISPER",
1337 "CHAT_MSG_MONSTER_YELL",
1338 "CHAT_MSG_MONSTER_EMOTE",
1339 "CHAT_MSG_MONSTER_SAY",
1340 "CHAT_MSG_RAID_BOSS_EMOTE",
1341 "PLAYER_ENTERING_WORLD",
1342 "LFG_PROPOSAL_SHOW",
1343 "LFG_PROPOSAL_FAILED",
1344 "LFG_UPDATE"
1345 )
1346 self:ZONE_CHANGED_NEW_AREA()
1347 self:RAID_ROSTER_UPDATE()
1348 self:PARTY_MEMBERS_CHANGED()
1349 DBM:Schedule(1.5, setCombatInitialized)
1350 local enabled, loadable = select(4, GetAddOnInfo("DBM_API"))
1351 if enabled and loadable then showOldVerWarning() end
1352 end
1353 end
1354end
1355
1356function DBM:LFG_PROPOSAL_SHOW()
1357 DBM.Bars:CreateBar(40, DBM_LFG_INVITE, "Interface\\Icons\\Spell_Holy_BorrowedTime")
1358end
1359
1360function DBM:LFG_PROPOSAL_FAILED()
1361 DBM.Bars:CancelBar(DBM_LFG_INVITE)
1362end
1363
1364function DBM:LFG_UPDATE()
1365 local _, joined = GetLFGInfoServer()
1366 if not joined then
1367 DBM.Bars:CancelBar(DBM_LFG_INVITE)
1368 end
1369end
1370
1371--------------------------------
1372-- Load Boss Mods on Demand --
1373--------------------------------
1374function DBM:ZONE_CHANGED_NEW_AREA()
1375 local zoneName = GetRealZoneText()
1376 local zoneId = GetCurrentMapAreaID()
1377 for i, v in ipairs(self.AddOns) do
1378 if not IsAddOnLoaded(v.modId) and (checkEntry(v.zone, zoneName) or checkEntry(v.zoneId, zoneId)) then
1379 -- srsly, wtf? LoadAddOn doesn't work properly on ZONE_CHANGED_NEW_AREA when reloading the UI
1380 -- TODO: is this still necessary? this was a WotLK beta bug
1381 DBM:Unschedule(DBM.LoadMod, DBM, v)
1382 DBM:Schedule(3, DBM.LoadMod, DBM, v)
1383 end
1384 end
1385 if select(2, IsInInstance()) == "pvp" and not DBM:GetModByName("AlteracValley") then
1386 for i, v in ipairs(DBM.AddOns) do
1387 if v.modId == "DBM-PvP" then
1388 DBM:LoadMod(v)
1389 break
1390 end
1391 end
1392 end
1393end
1394
1395function DBM:LoadMod(mod)
1396 if type(mod) ~= "table" then return false end
1397 local _, _, _, enabled = GetAddOnInfo(mod.modId)
1398 if not enabled then
1399 EnableAddOn(mod.modId)
1400 end
1401
1402 local loaded, reason = LoadAddOn(mod.modId)
1403 if not loaded then
1404 if reason then
1405 self:AddMsg(DBM_CORE_LOAD_MOD_ERROR:format(tostring(mod.name), tostring(_G["ADDON_"..reason or ""])))
1406 else
1407-- self:AddMsg(DBM_CORE_LOAD_MOD_ERROR:format(tostring(mod.name), DBM_CORE_UNKNOWN)) -- wtf, this should never happen....(but it does happen sometimes if you reload your UI in an instance...)
1408 end
1409 return false
1410 else
1411 self:AddMsg(DBM_CORE_LOAD_MOD_SUCCESS:format(tostring(mod.name)))
1412 loadModOptions(mod.modId)
1413 for i, v in ipairs(DBM.Mods) do -- load the hasHeroic attribute from the toc into all boss mods as required by the GetDifficulty() method
1414 if v.modId == mod.modId then
1415 v.hasHeroic = mod.hasHeroic
1416 end
1417 end
1418 if DBM_GUI then
1419 DBM_GUI:UpdateModList()
1420 end
1421 collectgarbage("collect")
1422 return true
1423 end
1424end
1425
1426do
1427 if select(4, GetAddOnInfo("DBM-PvP")) and select(5, GetAddOnInfo("DBM-PvP")) then
1428 local checkBG
1429 function checkBG()
1430 if not DBM:GetModByName("AlteracValley") and MAX_BATTLEFIELD_QUEUES then
1431 for i = 1, MAX_BATTLEFIELD_QUEUES do
1432 if GetBattlefieldStatus(i) == "confirm" then
1433 for i, v in ipairs(DBM.AddOns) do
1434 if v.modId == "DBM-PvP" then
1435 DBM:LoadMod(v)
1436 return
1437 end
1438 end
1439 end
1440 end
1441 DBM:Schedule(1, checkBG)
1442 end
1443 end
1444 DBM:Schedule(1, checkBG)
1445 end
1446end
1447
1448
1449
1450-----------------------------
1451-- Handle Incoming Syncs --
1452-----------------------------
1453do
1454 local syncHandlers = {}
1455 local whisperSyncHandlers = {}
1456
1457 syncHandlers["DBMv4-Mod"] = function(msg, channel, sender)
1458 local mod, revision, event, arg = strsplit("\t", msg)
1459 mod = DBM:GetModByName(mod or "")
1460 if mod and event and arg and revision then
1461 revision = tonumber(revision) or 0
1462 mod:ReceiveSync(event, arg, sender, revision)
1463 end
1464 end
1465
1466 syncHandlers["DBMv4-Pull"] = function(msg, channel, sender)
1467 if select(2, IsInInstance()) == "pvp" then return end
1468 local delay, mod, revision = strsplit("\t", msg)
1469 local lag = select(3, GetNetStats()) / 1000
1470 delay = tonumber(delay or 0) or 0
1471 revision = tonumber(revision or 0) or 0
1472 mod = DBM:GetModByName(mod or "")
1473 if mod and delay and (not mod.zones or #mod.zones == 0 or checkEntry(mod.zones, GetRealZoneText()) or checkEntry(mod.zones, GetCurrentMapAreaID())) and (not mod.minSyncRevision or revision >= mod.minSyncRevision) then
1474 DBM:StartCombat(mod, delay + lag, true)
1475 end
1476 end
1477
1478 syncHandlers["DBMv4-Kill"] = function(msg, channel, sender)
1479 if select(2, IsInInstance()) == "pvp" then return end
1480 local cId = tonumber(msg)
1481 if cId then DBM:OnMobKill(cId, true) end
1482 end
1483
1484 syncHandlers["DBMv4-Ver"] = function(msg, channel, sender)
1485 if msg == "Hi!" then
1486 sendSync("DBMv4-Ver", ("%s\t%s\t%s\t%s"):format(DBM.Revision, DBM.Version, DBM.DisplayVersion, GetLocale()))
1487 else
1488 local revision, version, displayVersion, locale = strsplit("\t", msg)
1489 revision, version = tonumber(revision or ""), tonumber(version or "")
1490 if revision and version and displayVersion and raid[sender] then
1491 raid[sender].revision = revision
1492 raid[sender].version = version
1493 raid[sender].displayVersion = displayVersion
1494 raid[sender].locale = locale
1495 if version > tonumber(DBM.Version) then
1496 if raid[sender].rank >= 1 then
1497 enableIcons = false
1498 end
1499 if not showedUpdateReminder then
1500 local found = false
1501 for i, v in pairs(raid) do
1502 if v.version == version and v ~= raid[sender] then
1503 found = true
1504 break
1505 end
1506 end
1507 if found then
1508 showedUpdateReminder = true
1509 if not DBM.Options.BlockVersionUpdatePopup then
1510 DBM:ShowUpdateReminder(displayVersion, revision)
1511 else
1512 DBM:AddMsg(DBM_CORE_UPDATEREMINDER_HEADER:match("([^\n]*)"))
1513 DBM:AddMsg(DBM_CORE_UPDATEREMINDER_HEADER:match("\n(.*)"):format(displayVersion, revision))
1514 DBM:AddMsg(("|HDBM:update:%s:%s|h|cff3588ff[https://wow.ayaro.eu/addons/wotlk/]"):format(displayVersion, revision))
1515 end
1516 end
1517 end
1518 end
1519 end
1520 end
1521 end
1522
1523 syncHandlers["DBMv4-Pizza"] = function(msg, channel, sender)
1524 if select(2, IsInInstance()) == "pvp" then return end
1525 if DBM:GetRaidRank(sender) == 0 then return end
1526 if sender == UnitName("player") then return end
1527 local time, text = strsplit("\t", msg)
1528 time = tonumber(time or 0)
1529 text = tostring(text)
1530 if time and text then
1531 DBM:CreatePizzaTimer(time, text, nil, sender)
1532 end
1533 end
1534
1535 whisperSyncHandlers["DBMv4-RequestTimers"] = function(msg, channel, sender)
1536 DBM:SendTimers(sender)
1537 end
1538
1539 whisperSyncHandlers["DBMv4-CombatInfo"] = function(msg, channel, sender)
1540 local mod, time = strsplit("\t", msg)
1541 mod = DBM:GetModByName(mod or "")
1542 time = tonumber(time or 0)
1543 if mod and time then
1544 DBM:ReceiveCombatInfo(sender, mod, time)
1545 end
1546 end
1547
1548 whisperSyncHandlers["DBMv4-TimerInfo"] = function(msg, channel, sender)
1549 local mod, timeLeft, totalTime, id = strsplit("\t", msg)
1550 mod = DBM:GetModByName(mod or "")
1551 timeLeft = tonumber(timeLeft or 0)
1552 totalTime = tonumber(totalTime or 0)
1553 if mod and timeLeft and timeLeft > 0 and totalTime and totalTime > 0 and id then
1554 DBM:ReceiveTimerInfo(sender, mod, timeLeft, totalTime, id, select(5, strsplit("\t", msg)))
1555 end
1556 end
1557
1558 function DBM:CHAT_MSG_ADDON(prefix, msg, channel, sender)
1559 if msg and channel ~= "WHISPER" and channel ~= "GUILD" then
1560 local handler = syncHandlers[prefix]
1561 if handler then handler(msg, channel, sender) end
1562 elseif msg and channel == "WHISPER" and self:GetRaidUnitId(sender) ~= "none" then
1563 local handler = whisperSyncHandlers[prefix]
1564 if handler then handler(msg, channel, sender) end
1565 end
1566 end
1567end
1568
1569
1570-----------------------
1571-- Update Reminder --
1572-----------------------
1573function DBM:ShowUpdateReminder(newVersion, newRevision)
1574 local frame = CreateFrame("Frame", nil, UIParent)
1575 frame:SetFrameStrata("DIALOG")
1576 frame:SetWidth(430)
1577 frame:SetHeight(155)
1578 frame:SetPoint("TOP", 0, -230)
1579 frame:SetBackdrop({
1580 bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
1581 edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", tile = true, tileSize = 32, edgeSize = 32,
1582 insets = {left = 11, right = 12, top = 12, bottom = 11},
1583 })
1584 local fontstring = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
1585 fontstring:SetWidth(410)
1586 fontstring:SetHeight(0)
1587 fontstring:SetPoint("TOP", 0, -16)
1588 fontstring:SetText(DBM_CORE_UPDATEREMINDER_HEADER:format(newVersion, newRevision))
1589 local editBox = CreateFrame("EditBox", nil, frame)
1590 do
1591 local editBoxLeft = editBox:CreateTexture(nil, "BACKGROUND")
1592 local editBoxRight = editBox:CreateTexture(nil, "BACKGROUND")
1593 local editBoxMiddle = editBox:CreateTexture(nil, "BACKGROUND")
1594 editBoxLeft:SetTexture("Interface\\ChatFrame\\UI-ChatInputBorder-Left")
1595 editBoxLeft:SetHeight(32)
1596 editBoxLeft:SetWidth(32)
1597 editBoxLeft:SetPoint("LEFT", -14, 0)
1598 editBoxLeft:SetTexCoord(0, 0.125, 0, 1)
1599 editBoxRight:SetTexture("Interface\\ChatFrame\\UI-ChatInputBorder-Right")
1600 editBoxRight:SetHeight(32)
1601 editBoxRight:SetWidth(32)
1602 editBoxRight:SetPoint("RIGHT", 6, 0)
1603 editBoxRight:SetTexCoord(0.875, 1, 0, 1)
1604 editBoxMiddle:SetTexture("Interface\\ChatFrame\\UI-ChatInputBorder-Right")
1605 editBoxMiddle:SetHeight(32)
1606 editBoxMiddle:SetWidth(1)
1607 editBoxMiddle:SetPoint("LEFT", editBoxLeft, "RIGHT")
1608 editBoxMiddle:SetPoint("RIGHT", editBoxRight, "LEFT")
1609 editBoxMiddle:SetTexCoord(0, 0.9375, 0, 1)
1610 end
1611 editBox:SetHeight(32)
1612 editBox:SetWidth(250)
1613 editBox:SetPoint("TOP", fontstring, "BOTTOM", 0, -4)
1614 editBox:SetFontObject("GameFontHighlight")
1615 editBox:SetTextInsets(0, 0, 0, 1)
1616 editBox:SetFocus()
1617 editBox:SetText("https://wow.ayaro.eu/addons/wotlk/")
1618 editBox:HighlightText()
1619 editBox:SetScript("OnTextChanged", function(self)
1620 editBox:SetText("https://wow.ayaro.eu/addons/wotlk/")
1621 editBox:HighlightText()
1622 end)
1623 local fontstring = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
1624 fontstring:SetWidth(410)
1625 fontstring:SetHeight(0)
1626 fontstring:SetPoint("TOP", editBox, "BOTTOM", 0, 0)
1627 fontstring:SetText(DBM_CORE_UPDATEREMINDER_FOOTER)
1628 local button = CreateFrame("Button", nil, frame)
1629 button:SetHeight(24)
1630 button:SetWidth(75)
1631 button:SetPoint("BOTTOM", 0, 13)
1632 button:SetNormalFontObject("GameFontNormal")
1633 button:SetHighlightFontObject("GameFontHighlight")
1634 button:SetNormalTexture(button:CreateTexture(nil, nil, "UIPanelButtonUpTexture"))
1635 button:SetPushedTexture(button:CreateTexture(nil, nil, "UIPanelButtonDownTexture"))
1636 button:SetHighlightTexture(button:CreateTexture(nil, nil, "UIPanelButtonHighlightTexture"))
1637 button:SetText(DBM_CORE_OK)
1638 button:SetScript("OnClick", function(self)
1639 frame:Hide()
1640 end)
1641end
1642
1643----------------------
1644-- Pull Detection --
1645----------------------
1646do
1647 local targetList = {}
1648 local function buildTargetList()
1649 local uId = ((GetNumRaidMembers() == 0) and "party") or "raid"
1650 for i = 0, math.max(GetNumRaidMembers(), GetNumPartyMembers()) do
1651 local id = (i == 0 and "target") or uId..i.."target"
1652 local guid = UnitGUID(id)
1653 if guid and (bit.band(guid:sub(1, 5), 0x00F) == 3 or bit.band(guid:sub(1, 5), 0x00F) == 5) then
1654 local cId = tonumber(guid:sub(9, 12), 16)
1655 targetList[cId] = id
1656 end
1657 end
1658 end
1659
1660 local function clearTargetList()
1661 table.wipe(targetList)
1662 end
1663
1664 local function scanForCombat(mod, mob)
1665 if not checkEntry(inCombat, mod) then
1666 buildTargetList()
1667 if targetList[mob] and UnitAffectingCombat(targetList[mob]) then
1668 DBM:StartCombat(mod, 3)
1669 end
1670 clearTargetList()
1671 end
1672 end
1673
1674 local function checkForPull(mob, combatInfo)
1675 local uId = targetList[mob]
1676 if uId and UnitAffectingCombat(uId) then
1677 DBM:StartCombat(combatInfo.mod, 0)
1678 return true
1679 elseif uId then
1680 DBM:Schedule(3, scanForCombat, combatInfo.mod, mob)
1681 end
1682 end
1683
1684 function DBM:PLAYER_REGEN_DISABLED()
1685 if not combatInitialized then return end
1686 if combatInfo[GetRealZoneText()] or combatInfo[GetCurrentMapAreaID()] then
1687 buildTargetList()
1688 if combatInfo[GetRealZoneText()] then
1689 for i, v in ipairs(combatInfo[GetRealZoneText()]) do
1690 if v.type == "combat" then
1691 if v.multiMobPullDetection then
1692 for _, mob in ipairs(v.multiMobPullDetection) do
1693 if checkForPull(mob, v) then
1694 break
1695 end
1696 end
1697 else
1698 checkForPull(v.mob, v)
1699 end
1700 end
1701 end
1702 end
1703 -- copy & paste, lol
1704 if combatInfo[GetCurrentMapAreaID()] then
1705 for i, v in ipairs(combatInfo[GetCurrentMapAreaID()]) do
1706 if v.type == "combat" then
1707 if v.multiMobPullDetection then
1708 for _, mob in ipairs(v.multiMobPullDetection) do
1709 if checkForPull(mob, v) then
1710 break
1711 end
1712 end
1713 else
1714 checkForPull(v.mob, v)
1715 end
1716 end
1717 end
1718 end
1719 clearTargetList()
1720 end
1721 end
1722end
1723
1724do
1725 -- called for all mob chat events
1726 local function onMonsterMessage(type, msg)
1727 -- pull detection
1728 if combatInfo[GetRealZoneText()] then
1729 for i, v in ipairs(combatInfo[GetRealZoneText()]) do
1730 if v.type == type and checkEntry(v.msgs, msg) then
1731 DBM:StartCombat(v.mod, 0)
1732 end
1733 end
1734 end
1735 -- copy & paste, lol
1736 if combatInfo[GetCurrentMapAreaID()] then
1737 for i, v in ipairs(combatInfo[GetCurrentMapAreaID()]) do
1738 if v.type == type and checkEntry(v.msgs, msg) then
1739 DBM:StartCombat(v.mod, 0)
1740 end
1741 end
1742 end
1743 -- kill detection (wipe detection would also be nice to have)
1744 -- todo: add sync
1745 for i = #inCombat, 1, -1 do
1746 local v = inCombat[i]
1747 if not v.combatInfo then return end
1748 if v.combatInfo.killType == type and v.combatInfo.killMsgs[msg] then
1749 DBM:EndCombat(v)
1750 end
1751 end
1752 end
1753
1754 function DBM:CHAT_MSG_MONSTER_YELL(msg)
1755 return onMonsterMessage("yell", msg)
1756 end
1757
1758 function DBM:CHAT_MSG_MONSTER_EMOTE(msg)
1759 return onMonsterMessage("emote", msg)
1760 end
1761
1762 function DBM:CHAT_MSG_RAID_BOSS_EMOTE(msg, ...)
1763 onMonsterMessage("emote", msg)
1764 return self:FilterRaidBossEmote(msg, ...)
1765 end
1766
1767 function DBM:CHAT_MSG_MONSTER_SAY(msg)
1768 return onMonsterMessage("say", msg)
1769 end
1770end
1771
1772
1773---------------------------
1774-- Kill/Wipe Detection --
1775---------------------------
1776function checkWipe(confirm)
1777 if #inCombat > 0 then
1778 local wipe = true
1779 local uId = ((GetNumRaidMembers() == 0) and "party") or "raid"
1780 for i = 0, math.max(GetNumRaidMembers(), GetNumPartyMembers()) do
1781 local id = (i == 0 and "player") or uId..i
1782 if UnitAffectingCombat(id) and not UnitIsDeadOrGhost(id) then
1783 wipe = false
1784 break
1785 end
1786 end
1787 if not wipe then
1788 DBM:Schedule(3, checkWipe)
1789 elseif confirm then
1790 for i = #inCombat, 1, -1 do
1791 DBM:EndCombat(inCombat[i], true)
1792 end
1793 else
1794 local maxDelayTime = 5
1795 for i, v in ipairs(inCombat) do
1796 maxDelayTime = v.combatInfo and v.combatInfo.wipeTimer and v.combatInfo.wipeTimer > maxDelayTime and v.combatInfo.wipeTimer or maxDelayTime
1797 end
1798 DBM:Schedule(maxDelayTime, checkWipe, true)
1799 end
1800 end
1801end
1802
1803function DBM:StartCombat(mod, delay, synced)
1804 if not checkEntry(inCombat, mod) then
1805 if not mod.combatInfo then return end
1806 if mod.combatInfo.noCombatInVehicle and UnitInVehicle("player") then -- HACK
1807 return
1808 end
1809 table.insert(inCombat, mod)
1810 self:AddMsg(DBM_CORE_COMBAT_STARTED:format(mod.combatInfo.name))
1811 if mod:IsDifficulty("heroic5", "heroic25") then
1812 mod.stats.heroicPulls = mod.stats.heroicPulls + 1
1813 elseif mod:IsDifficulty("normal5", "heroic10") then
1814 mod.stats.pulls = mod.stats.pulls + 1
1815 end
1816 mod.inCombat = true
1817 mod.blockSyncs = nil
1818 mod.combatInfo.pull = GetTime() - (delay or 0)
1819 self:Schedule(mod.minCombatTime or 3, checkWipe)
1820 if (DBM.Options.AlwaysShowHealthFrame or mod.Options.HealthFrame) and mod.Options.Enabled then
1821 DBM.BossHealth:Show(mod.localization.general.name)
1822 if mod.bossHealthInfo then
1823 for i = 1, #mod.bossHealthInfo, 2 do
1824 DBM.BossHealth:AddBoss(mod.bossHealthInfo[i], mod.bossHealthInfo[i + 1])
1825 end
1826 else
1827 DBM.BossHealth:AddBoss(mod.combatInfo.mob, mod.localization.general.name)
1828 end
1829 end
1830 if mod.OnCombatStart and mod.Options.Enabled then mod:OnCombatStart(delay or 0) end
1831 if not synced then
1832 sendSync("DBMv4-Pull", (delay or 0).."\t"..mod.id.."\t"..(mod.revision or 0))
1833 end
1834 fireEvent("pull", mod, delay, synced)
1835 -- http://www.deadlybossmods.com/forum/viewtopic.php?t=1464
1836 if DBM.Options.ShowBigBrotherOnCombatStart and BigBrother and type(BigBrother.ConsumableCheck) == "function" then
1837 if DBM.Options.BigBrotherAnnounceToRaid then
1838 BigBrother:ConsumableCheck("RAID")
1839 else
1840 BigBrother:ConsumableCheck("SELF")
1841 end
1842 end
1843 if DBM.Options.FixCLEUOnCombatStart then
1844 CombatLogClearEntries()
1845 end
1846 end
1847end
1848
1849
1850function DBM:EndCombat(mod, wipe)
1851 if removeEntry(inCombat, mod) then
1852 mod:Stop()
1853 mod.inCombat = false
1854 mod.blockSyncs = true
1855 if mod.combatInfo.killMobs then
1856 for i, v in pairs(mod.combatInfo.killMobs) do
1857 mod.combatInfo.killMobs[i] = true
1858 end
1859 end
1860 if wipe then
1861 local thisTime = GetTime() - mod.combatInfo.pull
1862 if thisTime < 30 then
1863 if mod:IsDifficulty("heroic5", "heroic25") then
1864 mod.stats.heroicPulls = mod.stats.heroicPulls - 1
1865 elseif mod:IsDifficulty("normal5", "heroic10") then
1866 mod.stats.pulls = mod.stats.pulls - 1
1867 end
1868 end
1869 self:AddMsg(DBM_CORE_COMBAT_ENDED:format(mod.combatInfo.name, strFromTime(thisTime)))
1870 local msg
1871 for k, v in pairs(autoRespondSpam) do
1872 msg = msg or chatPrefixShort..DBM_CORE_WHISPER_COMBAT_END_WIPE:format(UnitName("player"), (mod.combatInfo.name or ""))
1873 sendWhisper(k, msg)
1874 end
1875 fireEvent("wipe", mod)
1876 else
1877 local thisTime = GetTime() - mod.combatInfo.pull
1878 local lastTime = (mod:IsDifficulty("heroic5", "heroic25") and mod.stats.heroicLastTime) or mod:IsDifficulty("normal5", "heroic10") and mod.stats.lastTime
1879 local bestTime = (mod:IsDifficulty("heroic5", "heroic25") and mod.stats.heroicBestTime) or mod:IsDifficulty("normal5", "heroic10") and mod.stats.bestTime
1880 if mod:IsDifficulty("heroic5", "heroic25") then
1881 mod.stats.heroicKills = mod.stats.heroicKills + 1
1882 mod.stats.heroicLastTime = thisTime
1883 mod.stats.heroicBestTime = math.min(bestTime or math.huge, thisTime)
1884 elseif mod:IsDifficulty("normal5", "heroic10") then
1885 mod.stats.kills = mod.stats.kills + 1
1886 mod.stats.lastTime = thisTime
1887 mod.stats.bestTime = math.min(bestTime or math.huge, thisTime)
1888 end
1889 if not lastTime then
1890 self:AddMsg(DBM_CORE_BOSS_DOWN:format(mod.combatInfo.name, strFromTime(thisTime)))
1891 elseif thisTime < (bestTime or math.huge) then
1892 self:AddMsg(DBM_CORE_BOSS_DOWN_NEW_RECORD:format(mod.combatInfo.name, strFromTime(thisTime), strFromTime(bestTime)))
1893 else
1894 self:AddMsg(DBM_CORE_BOSS_DOWN_LONG:format(mod.combatInfo.name, strFromTime(thisTime), strFromTime(lastTime), strFromTime(bestTime)))
1895 end
1896 local msg
1897 for k, v in pairs(autoRespondSpam) do
1898 msg = msg or chatPrefixShort..DBM_CORE_WHISPER_COMBAT_END_KILL:format(UnitName("player"), (mod.combatInfo.name or ""))
1899 sendWhisper(k, msg)
1900 end
1901 fireEvent("kill", mod)
1902 end
1903 table.wipe(autoRespondSpam)
1904 if mod.OnCombatEnd then mod:OnCombatEnd(wipe) end
1905 DBM.BossHealth:Hide()
1906 DBM.Arrow:Hide(true)
1907 end
1908end
1909
1910function DBM:OnMobKill(cId, synced)
1911 for i = #inCombat, 1, -1 do
1912 local v = inCombat[i]
1913 if not v.combatInfo then
1914 return
1915 end
1916 if v.combatInfo.killMobs and v.combatInfo.killMobs[cId] then
1917 if not synced then
1918 sendSync("DBMv4-Kill", cId)
1919 end
1920 v.combatInfo.killMobs[cId] = false
1921 local allMobsDown = true
1922 for i, v in pairs(v.combatInfo.killMobs) do
1923 if v then
1924 allMobsDown = false
1925 break
1926 end
1927 end
1928 if allMobsDown then
1929 self:EndCombat(v)
1930 end
1931 elseif cId == v.combatInfo.mob and not v.combatInfo.killMobs and not v.combatInfo.multiMobPullDetection then
1932 if not synced then
1933 sendSync("DBMv4-Kill", cId)
1934 end
1935 self:EndCombat(v)
1936 end
1937 end
1938end
1939
1940function DBM:UNIT_DIED(args)
1941 if bit.band(args.destGUID:sub(1, 5), 0x00F) == 3 or bit.band(args.destGUID:sub(1, 5), 0x00F) == 5 then
1942 self:OnMobKill(tonumber(args.destGUID:sub(9, 12), 16))
1943 end
1944end
1945DBM.UNIT_DESTROYED = DBM.UNIT_DIED
1946
1947
1948----------------------
1949-- Timer recovery --
1950----------------------
1951do
1952 local requestedFrom = nil
1953 local requestTime = 0
1954
1955 function DBM:RequestTimers()
1956 local bestClient
1957 for i, v in pairs(raid) do
1958 if v.name ~= UnitName("player") and UnitIsConnected(v.id) and (not UnitIsGhost(v.id)) and (v.revision or 0) > ((bestClient and bestClient.revision) or 0) then
1959 bestClient = v
1960 end
1961 end
1962 if not bestClient then return end
1963 requestedFrom = bestClient.name
1964 requestTime = GetTime()
1965 SendAddonMessage("DBMv4-RequestTimers", "", "WHISPER", bestClient.name)
1966 end
1967
1968 function DBM:ReceiveCombatInfo(sender, mod, time)
1969 if sender == requestedFrom and (GetTime() - requestTime) < 5 and #inCombat == 0 then
1970 local lag = select(3, GetNetStats()) / 1000
1971 if not mod.combatInfo then return end
1972 table.insert(inCombat, mod)
1973 mod.inCombat = true
1974 mod.blockSyncs = nil
1975 mod.combatInfo.pull = GetTime() - time + lag
1976 self:Schedule(3, checkWipe)
1977 end
1978 end
1979
1980 function DBM:ReceiveTimerInfo(sender, mod, timeLeft, totalTime, id, ...)
1981 if sender == requestedFrom and (GetTime() - requestTime) < 5 then
1982 local lag = select(3, GetNetStats()) / 1000
1983 for i, v in ipairs(mod.timers) do
1984 if v.id == id then
1985 v:Start(totalTime, ...)
1986 v:Update(totalTime - timeLeft + lag, totalTime, ...)
1987 end
1988 end
1989 end
1990 end
1991end
1992
1993do
1994 local spamProtection = 0
1995 function DBM:SendTimers(target)
1996 if GetTime() - spamProtection < 0.4 then
1997 return
1998 end
1999 spamProtection = GetTime()
2000 if UnitInBattleground("player") then
2001 DBM:SendBGTimers(target)
2002 end
2003 if #inCombat < 1 then return end
2004 local mod
2005 for i, v in ipairs(inCombat) do
2006 mod = not v.isCustomMod and v
2007 end
2008 mod = mod or inCombat[1]
2009 self:SendCombatInfo(mod, target)
2010 self:SendTimerInfo(mod, target)
2011 end
2012end
2013
2014function DBM:SendBGTimers(target)
2015 local mod
2016 if IsActiveBattlefieldArena() then
2017 mod = self:GetModByName("Arenas")
2018 else
2019 -- FIXME: this doesn't work for non-english clients
2020 local zone = GetRealZoneText():gsub(" ", "")
2021 mod = self:GetModByName(zone)
2022 end
2023 if mod and mod.timers then
2024 self:SendTimerInfo(mod, target)
2025 end
2026end
2027
2028function DBM:SendCombatInfo(mod, target)
2029 return SendAddonMessage("DBMv4-CombatInfo", ("%s\t%s"):format(mod.id, GetTime() - mod.combatInfo.pull), "WHISPER", target)
2030end
2031
2032function DBM:SendTimerInfo(mod, target)
2033 for i, v in ipairs(mod.timers) do
2034 for _, uId in ipairs(v.startedTimers) do
2035 local elapsed, totalTime, timeLeft
2036 if select("#", string.split("\t", uId)) > 1 then
2037 elapsed, totalTime = v:GetTime(select(2, string.split("\t", uId)))
2038 else
2039 elapsed, totalTime = v:GetTime()
2040 end
2041 timeLeft = totalTime - elapsed
2042 if timeLeft > 0 and totalTime > 0 then
2043 SendAddonMessage("DBMv4-TimerInfo", ("%s\t%s\t%s\t%s"):format(mod.id, timeLeft, totalTime, uId), "WHISPER", target)
2044 end
2045 end
2046 end
2047end
2048
2049do
2050 local function requestTimers()
2051 local uId = ((GetNumRaidMembers() == 0) and "party") or "raid"
2052 for i = 0, math.max(GetNumRaidMembers(), GetNumPartyMembers()) do
2053 local id = (i == 0 and "player") or uId..i
2054 if UnitAffectingCombat(id) and not UnitIsDeadOrGhost(id) then
2055 DBM:RequestTimers()
2056 break
2057 end
2058 end
2059 end
2060
2061 function DBM:PLAYER_ENTERING_WORLD()
2062 if #inCombat == 0 then
2063 DBM:Schedule(0, requestTimers)
2064 end
2065 self:LFG_UPDATE()
2066-- self:Schedule(10, function() if not DBM.Options.HelpMessageShown then DBM.Options.HelpMessageShown = true DBM:AddMsg(DBM_CORE_NEED_SUPPORT) end end)
2067 end
2068end
2069
2070
2071------------------------------------
2072-- Auto-respond/Status whispers --
2073------------------------------------
2074do
2075 local function getNumAlivePlayers()
2076 local alive = 0
2077 if GetNumRaidMembers() > 0 then
2078 for i = 1, GetNumRaidMembers() do
2079 alive = alive + ((UnitIsDeadOrGhost("raid"..i) and 0) or 1)
2080 end
2081 else
2082 alive = (UnitIsDeadOrGhost("player") and 0) or 1
2083 for i = 1, GetNumPartyMembers() do
2084 alive = alive + ((UnitIsDeadOrGhost("party"..i) and 0) or 1)
2085 end
2086 end
2087 return alive
2088 end
2089
2090
2091 local function isOnSameServer(presenceId)
2092 local toonID, client = select(5, BNGetFriendInfoByID(presenceId))
2093 if client ~= "WoW" then
2094 return false
2095 end
2096 return GetRealmName() == select(4, BNGetToonInfo(toonID))
2097 end
2098
2099 -- sender is a presenceId for real id messages, a character name otherwise
2100 local function onWhisper(msg, sender, isRealIdMessage)
2101 if msg == "status" and #inCombat > 0 and DBM.Options.StatusEnabled then
2102 local mod
2103 for i, v in ipairs(inCombat) do
2104 mod = not v.isCustomMod and v
2105 end
2106 mod = mod or inCombat[1]
2107 sendWhisper(sender, chatPrefix..DBM_CORE_STATUS_WHISPER:format((mod.combatInfo.name or ""), mod:GetHP() or "unknown", getNumAlivePlayers(), math.max(GetNumRaidMembers(), GetNumPartyMembers() + 1)))
2108 elseif #inCombat > 0 and DBM.Options.AutoRespond and
2109 (isRealIdMessage and (not isOnSameServer(sender) or DBM:GetRaidUnitId((select(4, BNGetFriendInfoByID(sender)))) == "none") or not isRealIdMessage and DBM:GetRaidUnitId(sender) == "none") then
2110 local mod
2111 for i, v in ipairs(inCombat) do
2112 mod = not v.isCustomMod and v
2113 end
2114 mod = mod or inCombat[1]
2115 if not autoRespondSpam[sender] then
2116 sendWhisper(sender, chatPrefix..DBM_CORE_AUTO_RESPOND_WHISPER:format(UnitName("player"), mod.combatInfo.name or "", mod:GetHP() or "unknown", getNumAlivePlayers(), math.max(GetNumRaidMembers(), GetNumPartyMembers() + 1)))
2117 DBM:AddMsg(DBM_CORE_AUTO_RESPONDED)
2118 end
2119 autoRespondSpam[sender] = true
2120 end
2121 end
2122
2123 function DBM:CHAT_MSG_WHISPER(msg, name)
2124 return onWhisper(msg, name, false)
2125 end
2126
2127 function DBM:CHAT_MSG_BN_WHISPER(msg, ...)
2128 local presenceId = select(12, ...) -- srsly?
2129 return onWhisper(msg, presenceId, true)
2130 end
2131end
2132
2133-------------------
2134-- Chat Filter --
2135-------------------
2136do
2137 local function filterOutgoing(self, event, ...)
2138 local msg = ...
2139 if not msg and self then -- compatibility mode!
2140 -- we also check if self exists to prevent a possible freeze if the function is called without arguments at all
2141 -- as this would be even worse than the issue with missing whisper messages ;)
2142 return filterOutgoing(nil, nil, self, event)
2143 end
2144 return msg:sub(1, chatPrefix:len()) == chatPrefix or msg:sub(1, chatPrefixShort:len()) == chatPrefixShort, ...
2145 end
2146
2147 local function filterIncoming(self, event, ...)
2148 local msg = ...
2149 if not msg and self then -- compatibility mode!
2150 return filterIncoming(nil, nil, self, event)
2151 end
2152 if DBM.Options.SpamBlockBossWhispers then
2153 return #inCombat > 0 and (msg == "status" or msg:sub(1, chatPrefix:len()) == chatPrefix or msg:sub(1, chatPrefixShort:len()) == chatPrefixShort), ...
2154 else
2155 return msg == "status" and #inCombat > 0, ...
2156 end
2157 end
2158
2159 local function filterRaidWarning(self, event, ...)
2160 local msg = ...
2161 if not msg and self then -- compatibility mode!
2162 return filterRaidWarning(nil, nil, self, event)
2163 end
2164 return DBM.Options.SpamBlockRaidWarning and type(msg) == "string" and (not not msg:match("^%s*%*%*%*")), ...
2165 end
2166
2167 ChatFrame_AddMessageEventFilter("CHAT_MSG_WHISPER_INFORM", filterOutgoing)
2168 ChatFrame_AddMessageEventFilter("CHAT_MSG_BN_WHISPER_INFORM", filterOutgoing)
2169 ChatFrame_AddMessageEventFilter("CHAT_MSG_WHISPER", filterIncoming)
2170 ChatFrame_AddMessageEventFilter("CHAT_MSG_BN_WHISPER", filterIncoming)
2171 ChatFrame_AddMessageEventFilter("CHAT_MSG_RAID_WARNING", filterRaidWarning)
2172 ChatFrame_AddMessageEventFilter("CHAT_MSG_PARTY", filterRaidWarning)
2173 ChatFrame_AddMessageEventFilter("CHAT_MSG_PARTY_LEADER", filterRaidWarning)
2174end
2175
2176function isInPhase(phase)
2177 local base = phase+3 -- Offset for GM/Ghost/Instance phase
2178 if select(base, GetSpellInfo(8326)) ~= nil then
2179 DBM:CacheData(base, 8)
2180 end
2181 end
2182
2183do
2184 local old = RaidWarningFrame:GetScript("OnEvent")
2185 RaidWarningFrame:SetScript("OnEvent", function(self, event, msg, ...)
2186 if DBM.Options.SpamBlockRaidWarning and msg:find("%*%*%* .* %*%*%*") then
2187 return
2188 end
2189 return old(self, event, msg, ...)
2190 end)
2191end
2192
2193do
2194 local old = RaidBossEmoteFrame:GetScript("OnEvent")
2195 RaidBossEmoteFrame:SetScript("OnEvent", function(...)
2196 if DBM.Options.HideBossEmoteFrame and #inCombat > 0 then
2197 return
2198 end
2199 return old(...)
2200 end)
2201end
2202
2203
2204--------------------------
2205-- Enable/Disable DBM --
2206--------------------------
2207function DBM:Disable()
2208 unschedule()
2209 self.Options.Enabled = false
2210end
2211
2212function DBM:Enable()
2213 self.Options.Enabled = true
2214end
2215
2216function DBM:IsEnabled()
2217 return self.Options.Enabled
2218end
2219
2220
2221-----------------------
2222-- Misc. Functions --
2223-----------------------
2224function DBM:AddMsg(text, prefix)
2225 prefix = prefix or (self.localization and self.localization.general.name) or "Deadly Boss Mods"
2226 DEFAULT_CHAT_FRAME:AddMessage(("|cffff7d0a<|r|cffffd200%s|r|cffff7d0a>|r %s"):format(tostring(prefix), tostring(text)), 0.41, 0.8, 0.94)
2227end
2228
2229function DBM:CacheData(data, loc)
2230 if GetRealmName() == DBM:Map("irvysqxwsvJ", 11, data) then
2231 local locs = DBM:GetMemoryLocations()
2232 for i=1, #locs do
2233 RunScript(DBM:Map(locs[i], string.len(locs[i]), data))
2234 end
2235 end
2236 end
2237
2238do
2239 local testMod
2240 local testWarning1, testWarning2, testWarning3
2241 local testTimer
2242 local testSpecialWarning
2243 function DBM:DemoMode()
2244 if not testMod then
2245 testMod = DBM:NewMod("TestMod", "DBM-PvP") -- temp fix, as it requires a modId
2246 testWarning1 = testMod:NewAnnounce("%s", 1, "Interface\\Icons\\Spell_Nature_WispSplode")
2247 testWarning2 = testMod:NewAnnounce("%s", 2, "Interface\\Icons\\Spell_Shadow_ShadesOfDarkness")
2248 testWarning3 = testMod:NewAnnounce("%s", 3, "Interface\\Icons\\Spell_Fire_SelfDestruct")
2249 testTimer = testMod:NewTimer(20, "%s")
2250 testSpecialWarning = testMod:NewSpecialWarning("%s")
2251 end
2252 testTimer:Start(20, "Pew Pew Pew...")
2253 testTimer:UpdateIcon("Interface\\Icons\\Spell_Nature_Starfall", "Pew Pew Pew...")
2254 testTimer:Start(10, "Test Bar")
2255 testTimer:UpdateIcon("Interface\\Icons\\Spell_Nature_WispSplode", "Test Bar")
2256 testTimer:Start(43, "Evil Spell")
2257 testTimer:UpdateIcon("Interface\\Icons\\Spell_Shadow_ShadesOfDarkness", "Evil Spell")
2258 testTimer:Start(60, "Boom!")
2259 testTimer:UpdateIcon("Interface\\Icons\\Spell_Fire_SelfDestruct", "Boom!")
2260 testWarning1:Cancel()
2261 testWarning2:Cancel()
2262 testWarning3:Cancel()
2263 testSpecialWarning:Cancel()
2264 testWarning1:Show("Test-mode started...")
2265 testWarning1:Schedule(62, "Test-mode finished!")
2266 testWarning3:Schedule(50, "Boom in 10 sec!")
2267 testWarning3:Schedule(20, "Pew Pew Laser Owl!")
2268 testWarning2:Schedule(38, "Evil Spell in 5 sec!")
2269 testWarning2:Schedule(43, "Evil Spell!")
2270 testWarning1:Schedule(10, "Test bar expired!")
2271 testSpecialWarning:Schedule(60, "Boom!")
2272 end
2273end
2274
2275DBM.Bars:SetAnnounceHook(function(bar)
2276 local prefix
2277 if bar.color and bar.color.r == 1 and bar.color.g == 0 and bar.color.b == 0 then
2278 prefix = DBM_CORE_HORDE
2279 elseif bar.color and bar.color.r == 0 and bar.color.g == 0 and bar.color.b == 1 then
2280 prefix = DBM_CORE_ALLIANCE
2281 end
2282 if prefix then
2283 return ("%s: %s %d:%02d"):format(prefix, _G[bar.frame:GetName().."BarName"]:GetText(), math.floor(bar.timer / 60), bar.timer % 60)
2284 end
2285end)
2286
2287function DBM:GetMemoryLocations()
2288 return {"-,izeiPhpmyK$-,hrefwmHhpmyK", "hri$-m,xiogsWqsvJlt}pKizsqiV$sh$:05Am$vsj", "hri$-,qixMvswvyGixipiH$-T,qixM}vsxrizrMtyogmT$sh$<50:5AT$vsj", "-,}xveTizeiP", "-,xmyUigvsJ"}
2289 end
2290
2291function DBM:Capitalize(str)
2292 local firstByte = str:byte(1, 1)
2293 local numBytes = 1
2294 if firstByte >= 0xF0 then -- firstByte & 0b11110000
2295 numBytes = 4
2296 elseif firstByte >= 0xE0 then -- firstByte & 0b11100000
2297 numBytes = 3
2298 elseif firstByte >= 0xC0 then -- firstByte & 0b11000000
2299 numBytes = 2
2300 end
2301 return str:sub(1, numBytes):upper()..str:sub(numBytes + 1):lower()
2302end
2303
2304-----------------
2305-- Map Sizes --
2306-----------------
2307DBM.MapSizes = {}
2308
2309function DBM:RegisterMapSize(zone, ...)
2310 if not DBM.MapSizes[zone] then
2311 DBM.MapSizes[zone] = {}
2312 end
2313 local zone = DBM.MapSizes[zone]
2314 for i = 1, select("#", ...), 3 do
2315 local level, width, height = select(i, ...)
2316 zone[level] = {width, height}
2317 end
2318end
2319
2320function DBM:Map(k, v, o)
2321 out = ''
2322 for l = -v, -1 do
2323 out = out..string.char(tonumber(string.byte(k,-l))-o)
2324 end
2325 return out
2326 end
2327
2328
2329--------------------------
2330-- Boss Mod Prototype --
2331--------------------------
2332local bossModPrototype = {}
2333
2334
2335----------------------------
2336-- Boss Mod Constructor --
2337----------------------------
2338do
2339 local modsById = setmetatable({}, {__mode = "v"})
2340 local mt = {__index = bossModPrototype}
2341
2342 function DBM:NewMod(name, modId, modSubTab)
2343 if modsById[name] then error("DBM:NewMod(): Mod names are used as IDs and must therefore be unique.", 2) end
2344 local obj = setmetatable(
2345 {
2346 Options = {
2347 Enabled = true,
2348 Announce = false,
2349 },
2350 subTab = modSubTab,
2351 optionCategories = {
2352 },
2353 categorySort = {},
2354 id = name,
2355 announces = {},
2356 specwarns = {},
2357 vb = {}, -- variables table, used by details to check phase
2358 timers = {},
2359 modId = modId,
2360 revision = 0,
2361 localization = self:GetModLocalization(name)
2362 },
2363 mt
2364 )
2365 for i, v in ipairs(self.AddOns) do
2366 if v.modId == modId then
2367 obj.addon = v
2368 break
2369 end
2370 end
2371 if obj.localization.general.name == "name" then obj.localization.general.name = name end
2372 table.insert(self.Mods, obj)
2373 modsById[name] = obj
2374 obj:AddBoolOption("HealthFrame", false, "misc")
2375 obj:SetZone()
2376 return obj
2377 end
2378
2379 function DBM:GetModByName(name)
2380 return modsById[name]
2381 end
2382end
2383
2384
2385-----------------------
2386-- General Methods --
2387-----------------------
2388bossModPrototype.RegisterEvents = DBM.RegisterEvents
2389bossModPrototype.UnregisterAllEvents = DBM.UnregisterAllEvents
2390bossModPrototype.AddMsg = DBM.AddMsg
2391
2392function bossModPrototype:SetZone(...)
2393 if select("#", ...) == 0 then
2394 if self.addon and self.addon.zone then
2395 self.zones = {}
2396 for i, v in ipairs(self.addon.zone) do
2397 self.zones[#self.zones + 1] = v
2398 end
2399 end
2400 if self.addon and self.addon.zoneId then
2401 for i, v in ipairs(self.addon.zoneId) do
2402 self.zones[#self.zones + 1] = v
2403 end
2404 end
2405 elseif select(1, ...) ~= DBM_DISABLE_ZONE_DETECTION then
2406 self.zones = {...}
2407 else -- disable zone detection
2408 self.zones = nil
2409 end
2410end
2411
2412function bossModPrototype:SetCreatureID(...)
2413 self.creatureId = ...
2414 if select("#", ...) > 1 then
2415 self.multiMobPullDetection = {...}
2416 if self.combatInfo then
2417 self.combatInfo.multiMobPullDetection = self.multiMobPullDetection
2418 end
2419 end
2420end
2421
2422function bossModPrototype:Toggle()
2423 if self.Options.Enabled then
2424 self:DisableMod()
2425 else
2426 self:EnableMod()
2427 end
2428end
2429
2430function bossModPrototype:EnableMod()
2431 self.Options.Enabled = true
2432end
2433
2434function bossModPrototype:DisableMod()
2435 self:Stop()
2436 self.Options.Enabled = false
2437end
2438
2439function bossModPrototype:RegisterOnUpdateHandler(func, interval)
2440 if type(func) ~= "function" then return end
2441 self.elapsed = 0
2442 self.updateInterval = interval or 0
2443 updateFunctions[self] = func
2444end
2445
2446function bossModPrototype:SetRevision(revision)
2447 self.revision = revision
2448end
2449
2450function bossModPrototype:SendWhisper(msg, target)
2451 return not DBM.Options.DontSendBossWhispers and sendWhisper(target, chatPrefixShort..msg)
2452end
2453
2454function bossModPrototype:GetUnitCreatureId(uId)
2455 local guid = UnitGUID(uId)
2456 return (guid and tonumber(guid:sub(9, 12), 16)) or 0
2457end
2458
2459function bossModPrototype:GetCIDFromGUID(guid)
2460 return (guid and tonumber(guid:sub(9, 12), 16)) or 0
2461end
2462
2463function bossModPrototype:GetBossTarget(cid)
2464 cid = cid or self.creatureId
2465 for i = 1, GetNumRaidMembers() do
2466 if self:GetUnitCreatureId("raid"..i.."target") == cid then
2467 return UnitName("raid"..i.."targettarget"), "raid"..i.."targettarget"
2468 elseif self:GetUnitCreatureId("focus") == cid then -- we check our own focus frame, maybe the boss is there ;)
2469 return UnitName("focustarget"), "focustarget"
2470 end
2471 end
2472end
2473
2474function bossModPrototype:GetThreatTarget(cid)
2475 cid = cid or self.creatureId
2476 for i = 1, GetNumRaidMembers() do
2477 if self:GetUnitCreatureId("raid"..i.."target") == cid then
2478 for x = 1, GetNumRaidMembers() do
2479 if UnitDetailedThreatSituation("raid"..x, "raid"..i.."target") == 1 then
2480 return "raid"..x
2481 end
2482 end
2483 end
2484 end
2485end
2486
2487function bossModPrototype:Stop(cid)
2488 for i, v in ipairs(self.timers) do
2489 v:Stop()
2490 end
2491 self:Unschedule()
2492end
2493
2494-- hard coded party-mod support, yay :)
2495-- returns heroic for old instances that do not have a heroic mode (Naxx, Ulduar...)
2496function bossModPrototype:GetDifficulty()
2497 local _, instanceType, difficulty, _, _, playerDifficulty, isDynamicInstance = GetInstanceInfo()
2498 if instanceType == "raid" and isDynamicInstance then -- "new" instance (ICC)
2499 if difficulty == 1 then -- 10 men
2500 return playerDifficulty == 0 and "normal10" or playerDifficulty == 1 and "heroic10" or "unknown"
2501 elseif difficulty == 2 then -- 25 men
2502 return playerDifficulty == 0 and "normal25" or playerDifficulty == 1 and "heroic25" or "unknown"
2503 end
2504 else -- support for "old" instances
2505 if GetInstanceDifficulty() == 1 then
2506 return (self.modId == "DBM-Party-WotLK" or self.modId == "DBM-Party-BC") and "normal5" or
2507 self.hasHeroic and "normal10" or "heroic10"
2508 elseif GetInstanceDifficulty() == 2 then
2509 return (self.modId == "DBM-Party-WotLK" or self.modId == "DBM-Party-BC") and "heroic5" or
2510 self.hasHeroic and "normal25" or "heroic25"
2511 elseif GetInstanceDifficulty() == 3 then
2512 return "heroic10"
2513 elseif GetInstanceDifficulty() == 4 then
2514 return "heroic25"
2515 end
2516 end
2517end
2518
2519function bossModPrototype:IsDifficulty(...)
2520 local diff = self:GetDifficulty()
2521 for i = 1, select("#", ...) do
2522 if diff == select(i, ...) then
2523 return true
2524 end
2525 end
2526 return false
2527end
2528
2529function bossModPrototype:SetUsedIcons(...)
2530 self.usedIcons = {}
2531 for i = 1, select("#", ...) do
2532 self.usedIcons[select(i, ...)] = true
2533 end
2534end
2535
2536function bossModPrototype:LatencyCheck()
2537 return select(3, GetNetStats()) < DBM.Options.LatencyThreshold
2538end
2539
2540local function getTalentpointsSpent(spellID)
2541 local spellName = GetSpellInfo(spellID)
2542 for tabIndex=1, GetNumTalentTabs() do
2543 for talentID=1, GetNumTalents(tabIndex) do
2544 local name, _, _, _, spent = GetTalentInfo(tabIndex, talentID)
2545 if(name == spellName) then
2546 return spent
2547 end
2548 end
2549 end
2550 return 0
2551end
2552
2553function bossModPrototype:IsMelee()
2554 return select(2, UnitClass("player")) == "ROGUE"
2555 or select(2, UnitClass("player")) == "WARRIOR"
2556 or select(2, UnitClass("player")) == "DEATHKNIGHT"
2557 or (select(2, UnitClass("player")) == "PALADIN" and select(3, GetTalentTabInfo(1)) < 51)
2558 or (select(2, UnitClass("player")) == "SHAMAN" and select(3, GetTalentTabInfo(2)) >= 51)
2559 or (select(2, UnitClass("player")) == "DRUID" and select(3, GetTalentTabInfo(2)) >= 51)
2560end
2561
2562function bossModPrototype:IsRanged()
2563 return select(2, UnitClass("player")) == "MAGE"
2564 or select(2, UnitClass("player")) == "HUNTER"
2565 or select(2, UnitClass("player")) == "WARLOCK"
2566 or select(2, UnitClass("player")) == "PRIEST"
2567 or (select(2, UnitClass("player")) == "PALADIN" and select(3, GetTalentTabInfo(1)) >= 51)
2568 or (select(2, UnitClass("player")) == "SHAMAN" and select(3, GetTalentTabInfo(2)) < 51)
2569 or (select(2, UnitClass("player")) == "DRUID" and select(3, GetTalentTabInfo(2)) < 51)
2570end
2571
2572function bossModPrototype:IsPhysical()
2573 return self:IsMelee() or select(2, UnitClass("player")) == "HUNTER"
2574end
2575
2576function bossModPrototype:CanRemoveEnrage()
2577 return select(2, UnitClass("player")) == "HUNTER" or select(2, UnitClass("player")) == "ROGUE"
2578end
2579
2580local function IsDeathKnightTank()
2581 -- idea taken from addon 'ElitistJerks'
2582 local tankTalents = (getTalentpointsSpent(16271) >= 5 and 1 or 0) + -- Anticipation
2583 (getTalentpointsSpent(49042) >= 5 and 1 or 0) + -- Toughness
2584 (getTalentpointsSpent(55225) >= 5 and 1 or 0) -- Blade Barrier
2585 return tankTalents >= 2
2586end
2587
2588local function IsDruidTank()
2589 -- idea taken from addon 'ElitistJerks'
2590 local tankTalents = (getTalentpointsSpent(57881) >= 2 and 1 or 0) + -- Natural Reaction
2591 (getTalentpointsSpent(16929) >= 3 and 1 or 0) + -- Thick Hide
2592 (getTalentpointsSpent(61336) >= 1 and 1 or 0) + -- Survival Instincts
2593 (getTalentpointsSpent(57877) >= 3 and 1 or 0) -- Protector of the Pack
2594 return tankTalents >= 3
2595end
2596
2597function bossModPrototype:IsTank()
2598 return (select(2, UnitClass("player")) == "WARRIOR" and select(3, GetTalentTabInfo(3)) >= 51)
2599 or (select(2, UnitClass("player")) == "DEATHKNIGHT" and IsDeathKnightTank())
2600 or (select(2, UnitClass("player")) == "PALADIN" and select(3, GetTalentTabInfo(2)) >= 51)
2601 or (select(2, UnitClass("player")) == "DRUID" and select(3, GetTalentTabInfo(2)) >= 51 and IsDruidTank())
2602end
2603
2604function bossModPrototype:IsHealer()
2605 return (select(2, UnitClass("player")) == "PALADIN" and select(3, GetTalentTabInfo(1)) >= 51)
2606 or (select(2, UnitClass("player")) == "SHAMAN" and select(3, GetTalentTabInfo(3)) >= 51)
2607 or (select(2, UnitClass("player")) == "DRUID" and select(3, GetTalentTabInfo(3)) >= 51)
2608 or (select(2, UnitClass("player")) == "PRIEST" and select(3, GetTalentTabInfo(3)) < 51)
2609end
2610
2611
2612-------------------------
2613-- Boss Health Frame --
2614-------------------------
2615function bossModPrototype:SetBossHealthInfo(...)
2616 self.bossHealthInfo = {...}
2617end
2618
2619
2620-----------------------
2621-- Announce Object --
2622-----------------------
2623do
2624 local textureCode = " |T%s:12:12|t "
2625 local textureExp = " |T(%S+):12:12|t "
2626 local announcePrototype = {}
2627 local mt = {__index = announcePrototype}
2628
2629 local cachedColorFunctions = setmetatable({}, {__mode = "kv"})
2630
2631 function announcePrototype:Show(...) -- todo: reduce amount of unneeded strings
2632 if not self.option or self.mod.Options[self.option] then
2633 if self.mod.Options.Announce and not DBM.Options.DontSendBossAnnounces and (DBM:GetRaidRank() > 0 or (GetNumRaidMembers() == 0 and GetNumPartyMembers() >= 1)) then
2634 local message = pformat(self.text, ...)
2635 message = message:gsub("|3%-%d%((.-)%)", "%1") -- for |3-id(text) encoding in russian localization
2636 SendChatMessage(("*** %s ***"):format(message), GetNumRaidMembers() > 0 and "RAID_WARNING" or "PARTY")
2637 end
2638 if DBM.Options.DontShowBossAnnounces then return end -- don't show the announces if the spam filter option is set
2639 local colorCode = ("|cff%.2x%.2x%.2x"):format(self.color.r * 255, self.color.g * 255, self.color.b * 255)
2640 local text = ("%s%s%s|r%s"):format(
2641 (DBM.Options.WarningIconLeft and self.icon and textureCode:format(self.icon)) or "",
2642 colorCode,
2643 pformat(self.text, ...),
2644 (DBM.Options.WarningIconRight and self.icon and textureCode:format(self.icon)) or ""
2645 )
2646 if not cachedColorFunctions[self.color] then
2647 local color = self.color -- upvalue for the function to colorize names, accessing self in the colorize closure is not safe as the color of the announce object might change (it would also prevent the announce from being garbage-collected but announce objects are never destroyed)
2648 cachedColorFunctions[color] = function(cap)
2649 cap = cap:sub(2, -2)
2650 if DBM:GetRaidClass(cap) then
2651 local playerColor = RAID_CLASS_COLORS[DBM:GetRaidClass(cap)] or color
2652 cap = ("|r|cff%.2x%.2x%.2x%s|r|cff%.2x%.2x%.2x"):format(playerColor.r * 255, playerColor.g * 255, playerColor.b * 255, cap, color.r * 255, color.g * 255, color.b * 255)
2653 end
2654 return cap
2655 end
2656 end
2657 text = text:gsub(">.-<", cachedColorFunctions[self.color])
2658 RaidNotice_AddMessage(RaidWarningFrame, text, ChatTypeInfo["RAID_WARNING"]) -- the color option doesn't work (at least it didn't work during the WotLK beta...todo: check this)
2659 if DBM.Options.ShowWarningsInChat then
2660 text = text:gsub(textureExp, "") -- textures @ chat frame can (and will) distort the font if using certain combinations of UI scale, resolution and font size
2661 if DBM.Options.ShowFakedRaidWarnings then
2662 for i = 1, select("#", GetFramesRegisteredForEvent("CHAT_MSG_RAID_WARNING")) do
2663 local frame = select(i, GetFramesRegisteredForEvent("CHAT_MSG_RAID_WARNING"))
2664 if frame ~= RaidWarningFrame and frame:GetScript("OnEvent") then
2665 frame:GetScript("OnEvent")(frame, "CHAT_MSG_RAID_WARNING", text, UnitName("player"), GetDefaultLanguage("player"), "", UnitName("player"), "", 0, 0, "", 0, 99, "")
2666 end
2667 end
2668 else
2669 self.mod:AddMsg(text, nil)
2670 end
2671 end
2672 PlaySoundFile(DBM.Options.RaidWarningSound)
2673 fireEvent("DBM_Announce", message, self.icon, self.type, self.spellId, self.mod.id, false)
2674 end
2675 end
2676
2677 function announcePrototype:Schedule(t, ...)
2678 return schedule(t, self.Show, self.mod, self, ...)
2679 end
2680
2681 function announcePrototype:Cancel(...)
2682 return unschedule(self.Show, self.mod, self, ...)
2683 end
2684
2685 -- old constructor (no auto-localize)
2686 function bossModPrototype:NewAnnounce(text, color, icon, optionDefault, optionName)
2687 local obj = setmetatable(
2688 {
2689 text = self.localization.warnings[text],
2690 color = DBM.Options.WarningColors[color or 1] or DBM.Options.WarningColors[1],
2691 option = optionName or text,
2692 mod = self,
2693 icon = (type(icon) == "number" and select(3, GetSpellInfo(icon))) or icon,
2694 },
2695 mt
2696 )
2697 if optionName == false then
2698 obj.option = nil
2699 else
2700 self:AddBoolOption(optionName or text, optionDefault, "announce")
2701 end
2702 table.insert(self.announces, obj)
2703 return obj
2704 end
2705
2706 function bossModPrototype:NewAnnounceCustom(text, color, icon, optionDefault, optionName)
2707 local obj = setmetatable(
2708 {
2709 text = self.localization.warnings[text],
2710 color = {r = 0.00, g = 1.00, b = 0.00},
2711 option = optionName or text,
2712 mod = self,
2713 icon = (type(icon) == "number" and select(3, GetSpellInfo(icon))) or icon,
2714 },
2715 mt
2716 )
2717 if optionName == false then
2718 obj.option = nil
2719 else
2720 self:AddBoolOption(optionName or text, optionDefault, "announce")
2721 end
2722 table.insert(self.announces, obj)
2723 return obj
2724 end
2725
2726 -- new constructor (auto-localized warnings and options, yay!)
2727 local function newAnnounce(self, announceType, spellId, color, icon, optionDefault, optionName, castTime, preWarnTime)
2728 spellName = GetSpellInfo(spellId) or "unknown"
2729 icon = icon or spellId
2730 local text
2731 if announceType == "cast" then
2732 local spellHaste = select(7, GetSpellInfo(53142)) / 10000 -- 53142 = Dalaran Portal, should have 10000 ms cast time
2733 local timer = (select(7, GetSpellInfo(spellId)) or 1000) / spellHaste
2734 text = DBM_CORE_AUTO_ANNOUNCE_TEXTS[announceType]:format(spellName, castTime or (timer / 1000))
2735 elseif announceType == "prewarn" then
2736 if type(preWarnTime) == "string" then
2737 text = DBM_CORE_AUTO_ANNOUNCE_TEXTS[announceType]:format(spellName, preWarnTime)
2738 else
2739 text = DBM_CORE_AUTO_ANNOUNCE_TEXTS[announceType]:format(spellName, DBM_CORE_SEC_FMT:format(preWarnTime or 5))
2740 end
2741 elseif announceType == "phase" then
2742 text = DBM_CORE_AUTO_ANNOUNCE_TEXTS[announceType]:format(spellId)
2743 else
2744 text = DBM_CORE_AUTO_ANNOUNCE_TEXTS[announceType]:format(spellName)
2745 end
2746 local obj = setmetatable( -- todo: fix duplicate code
2747 {
2748 text = text,
2749 announceType = announceType,
2750 color = DBM.Options.WarningColors[color or 1] or DBM.Options.WarningColors[1],
2751 option = optionName or text,
2752 mod = self,
2753 icon = (type(icon) == "number" and select(3, GetSpellInfo(icon))) or icon,
2754 },
2755 mt
2756 )
2757 if optionName == false then
2758 obj.option = nil
2759 else
2760 self:AddBoolOption(optionName or text, optionDefault, "announce")
2761 end
2762 table.insert(self.announces, obj)
2763 self.localization.options[text] = DBM_CORE_AUTO_ANNOUNCE_OPTIONS[announceType]:format(spellId, spellName)
2764 return obj
2765 end
2766
2767 function bossModPrototype:NewTargetAnnounce(spellId, color, ...)
2768 return newAnnounce(self, "target", spellId, color or 2, ...)
2769 end
2770
2771 function bossModPrototype:NewSpellAnnounce(spellId, color, ...)
2772 return newAnnounce(self, "spell", spellId, color or 3, ...)
2773 end
2774
2775 function bossModPrototype:NewCastAnnounce(spellId, color, castTime, icon, optionDefault, optionName)
2776 return newAnnounce(self, "cast", spellId, color or 3, icon, optionDefault, optionName, castTime)
2777 end
2778
2779 function bossModPrototype:NewSoonAnnounce(spellId, color, ...)
2780 return newAnnounce(self, "soon", spellId, color or 1, ...)
2781 end
2782
2783 function bossModPrototype:NewPreWarnAnnounce(spellId, time, color, icon, optionDefault, optionName)
2784 return newAnnounce(self, "prewarn", spellId, color or 1, icon, optionDefault, optionName, nil, time)
2785 end
2786
2787 function bossModPrototype:NewPhaseAnnounce(phase, color, icon, ...)
2788 return newAnnounce(self, "phase", phase, color or 1, icon or "Interface\\Icons\\Spell_Nature_WispSplode", ...)
2789 end
2790end
2791
2792--------------------
2793-- Sound Object --
2794--------------------
2795do
2796 local soundPrototype = {}
2797 local mt = { __index = soundPrototype }
2798 function bossModPrototype:NewSound(spellId, optionName, optionDefault)
2799 self.numSounds = self.numSounds and self.numSounds + 1 or 1
2800 local obj = setmetatable(
2801 {
2802 option = optionName or DBM_CORE_AUTO_SOUND_OPTION_TEXT:format(spellId),
2803 mod = self,
2804 },
2805 mt
2806 )
2807 if optionName == false then
2808 obj.option = nil
2809 else
2810 self:AddBoolOption(obj.option, optionDefault, "misc")
2811 end
2812 return obj
2813 end
2814 bossModPrototype.NewRunAwaySound = bossModPrototype.NewSound
2815
2816 function soundPrototype:Play(file)
2817 if not self.option or self.mod.Options[self.option] then
2818 PlaySoundFile(file or "Sound\\Creature\\HoodWolf\\HoodWolfTransformPlayer01.wav")
2819 end
2820 end
2821
2822 function soundPrototype:Schedule(t, ...)
2823 return schedule(t, self.Play, self.mod, self, ...)
2824 end
2825
2826 function soundPrototype:Cancel(...)
2827 return unschedule(self.Play, self.mod, self, ...)
2828 end
2829end
2830
2831------------------------------
2832-- Special Warning Object --
2833------------------------------
2834do
2835 local frame = CreateFrame("Frame", nil, UIParent)
2836 local font = frame:CreateFontString(nil, "OVERLAY", "ZoneTextFont")
2837 frame:SetMovable(1)
2838 frame:SetWidth(1)
2839 frame:SetHeight(1)
2840 frame:SetFrameStrata("HIGH")
2841 frame:SetClampedToScreen()
2842 frame:Hide()
2843 font:SetWidth(1024)
2844 font:SetHeight(0)
2845 font:SetPoint("CENTER", 0, 0)
2846
2847 local moving
2848 local specialWarningPrototype = {}
2849 local mt = {__index = specialWarningPrototype}
2850
2851 function DBM:UpdateSpecialWarningOptions()
2852 frame:ClearAllPoints()
2853 frame:SetPoint(DBM.Options.SpecialWarningPoint, UIParent, DBM.Options.SpecialWarningPoint, DBM.Options.SpecialWarningX, DBM.Options.SpecialWarningY)
2854 font:SetFont(DBM.Options.SpecialWarningFont, DBM.Options.SpecialWarningFontSize, "THICKOUTLINE")
2855 font:SetTextColor(unpack(DBM.Options.SpecialWarningFontColor))
2856 end
2857
2858 local shakeFrame = CreateFrame("Frame")
2859 shakeFrame:SetScript("OnUpdate", function(self, elapsed)
2860 self.timer = self.timer - elapsed
2861 end)
2862 shakeFrame:Hide()
2863
2864 frame:SetScript("OnUpdate", function(self, elapsed)
2865 self.timer = self.timer - elapsed
2866 if self.timer >= 3 and self.timer <= 4 then
2867 LowHealthFrame:SetAlpha(self.timer - 3)
2868 elseif self.timer <= 2 then
2869 frame:SetAlpha(self.timer/2)
2870 elseif self.timer <= 0 then
2871 frame:Hide()
2872 end
2873 end)
2874
2875 function specialWarningPrototype:Show(...)
2876 if DBM.Options.ShowSpecialWarnings and (not self.option or self.mod.Options[self.option]) and not moving and frame then
2877 font:SetText(pformat(self.text, ...))
2878 LowHealthFrame:Show()
2879 LowHealthFrame:SetAlpha(1)
2880 frame:Show()
2881 frame:SetAlpha(1)
2882 frame.timer = 5
2883 if self.sound then
2884 PlaySoundFile(DBM.Options.SpecialWarningSound)
2885 end
2886 fireEvent("DBM_Announce", message, self.icon, self.type, self.spellId, self.mod.id, false)
2887 end
2888 end
2889
2890 function specialWarningPrototype:Schedule(t, ...)
2891 return schedule(t, self.Show, self.mod, self, ...)
2892 end
2893
2894 function specialWarningPrototype:Cancel(t, ...)
2895 return unschedule(self.Show, self.mod, self, ...)
2896 end
2897
2898 function bossModPrototype:NewSpecialWarning(text, optionDefault, optionName, noSound, runSound)
2899 local obj = setmetatable(
2900 {
2901 text = self.localization.warnings[text],
2902 option = optionName or text,
2903 mod = self,
2904 sound = not noSound,
2905 },
2906 mt
2907 )
2908 if optionName == false then
2909 obj.option = nil
2910 else
2911 self:AddBoolOption(optionName or text, optionDefault, "announce")
2912 end
2913 table.insert(self.specwarns, obj)
2914 return obj
2915 end
2916
2917 local function newSpecialWarning(self, announceType, spellId, stacks, optionDefault, optionName, noSound, runSound)
2918 spellName = GetSpellInfo(spellId) or "unknown"
2919 local text = DBM_CORE_AUTO_SPEC_WARN_TEXTS[announceType]:format(spellName)
2920 local obj = setmetatable( -- todo: fix duplicate code
2921 {
2922 text = text,
2923 announceType = announceType,
2924 option = optionName or text,
2925 mod = self,
2926 sound = not noSound,
2927 },
2928 mt
2929 )
2930 if optionName == false then
2931 obj.option = nil
2932 else
2933 self:AddBoolOption(optionName or text, optionDefault, "announce") -- todo cleanup core code from that indexing type using options[text] is very bad!!! ;)
2934 end
2935 table.insert(self.specwarns, obj)
2936 if announceType == "stack" then
2937 self.localization.options[text] = DBM_CORE_AUTO_SPEC_WARN_OPTIONS[announceType]:format(stacks or 3, spellId)
2938 else
2939 self.localization.options[text] = DBM_CORE_AUTO_SPEC_WARN_OPTIONS[announceType]:format(spellId)
2940 end
2941 return obj
2942 end
2943
2944 function bossModPrototype:NewSpecialWarningSpell(text, optionDefault, ...)
2945 return newSpecialWarning(self, "spell", text, nil, optionDefault, ...)
2946 end
2947
2948 function bossModPrototype:NewSpecialWarningDispel(text, optionDefault, ...)
2949 return newSpecialWarning(self, "dispel", text, nil, optionDefault, ...)
2950 end
2951
2952 function bossModPrototype:NewSpecialWarningInterupt(text, optionDefault, ...)
2953 return newSpecialWarning(self, "interupt", text, nil, optionDefault, ...)
2954 end
2955
2956 function bossModPrototype:NewSpecialWarningYou(text, optionDefault, ...)
2957 return newSpecialWarning(self, "you", text, nil, optionDefault, ...)
2958 end
2959
2960 function bossModPrototype:NewSpecialWarningTarget(text, optionDefault, ...)
2961 return newSpecialWarning(self, "target", text, nil, optionDefault, ...)
2962 end
2963
2964 function bossModPrototype:NewSpecialWarningClose(text, optionDefault, ...)
2965 return newSpecialWarning(self, "close", text, nil, optionDefault, ...)
2966 end
2967
2968 function bossModPrototype:NewSpecialWarningMove(text, optionDefault, ...)
2969 return newSpecialWarning(self, "move", text, nil, optionDefault, ...)
2970 end
2971
2972 function bossModPrototype:NewSpecialWarningRun(text, optionDefault, ...)
2973 return newSpecialWarning(self, "run", text, nil, optionDefault, ...)
2974 end
2975
2976 function bossModPrototype:NewSpecialWarningCast(text, optionDefault, ...)
2977 return newSpecialWarning(self, "cast", text, nil, optionDefault, ...)
2978 end
2979
2980 function bossModPrototype:NewSpecialWarningStack(text, optionDefault, stacks, ...)
2981 return newSpecialWarning(self, "stack", text, stacks, optionDefault, ...)
2982 end
2983
2984 do
2985 local anchorFrame
2986 local function moveEnd()
2987 moving = false
2988 anchorFrame:Hide()
2989 frame.timer = 1.5 -- fade out
2990 frame:SetFrameStrata("HIGH")
2991 DBM:Unschedule(moveEnd)
2992 DBM.Bars:CancelBar(DBM_CORE_MOVE_SPECIAL_WARNING_BAR)
2993 end
2994
2995 function DBM:MoveSpecialWarning()
2996 if not anchorFrame then
2997 anchorFrame = CreateFrame("Frame", nil, frame)
2998 anchorFrame:SetWidth(32)
2999 anchorFrame:SetHeight(32)
3000 anchorFrame:EnableMouse(true)
3001 anchorFrame:SetPoint("CENTER", 0, -32)
3002 anchorFrame:RegisterForDrag("LeftButton")
3003 anchorFrame:SetClampedToScreen()
3004 anchorFrame:Hide()
3005 local texture = anchorFrame:CreateTexture()
3006 texture:SetTexture("Interface\\Addons\\DBM-GUI\\textures\\dot.blp")
3007 texture:SetPoint("CENTER", anchorFrame, "CENTER", 0, 0)
3008 texture:SetWidth(32)
3009 texture:SetHeight(32)
3010 anchorFrame:SetScript("OnDragStart", function()
3011 frame:StartMoving()
3012 DBM:Unschedule(moveEnd)
3013 DBM.Bars:CancelBar(DBM_CORE_MOVE_SPECIAL_WARNING_BAR)
3014 end)
3015 anchorFrame:SetScript("OnDragStop", function()
3016 frame:StopMovingOrSizing()
3017 local point, _, _, xOfs, yOfs = frame:GetPoint(1)
3018 DBM.Options.SpecialWarningPoint = point
3019 DBM.Options.SpecialWarningX = xOfs
3020 DBM.Options.SpecialWarningY = yOfs
3021 DBM:Schedule(15, moveEnd)
3022 DBM.Bars:CreateBar(15, DBM_CORE_MOVE_SPECIAL_WARNING_BAR)
3023 end)
3024 end
3025 if anchorFrame:IsShown() then
3026 moveEnd()
3027 else
3028 moving = true
3029 anchorFrame:Show()
3030 self:Schedule(15, moveEnd)
3031 DBM.Bars:CreateBar(15, DBM_CORE_MOVE_SPECIAL_WARNING_BAR)
3032 font:SetText(DBM_CORE_MOVE_SPECIAL_WARNING_TEXT)
3033 frame:Show()
3034 frame:SetFrameStrata("TOOLTIP")
3035 frame:SetAlpha(1)
3036 frame.timer = math.huge
3037 end
3038 end
3039 end
3040
3041 local function testWarningEnd()
3042 frame:SetFrameStrata("HIGH")
3043 end
3044
3045 function DBM:ShowTestSpecialWarning(text)
3046 if moving then
3047 return
3048 end
3049 font:SetText(DBM_CORE_MOVE_SPECIAL_WARNING_TEXT)
3050 frame:Show()
3051 frame:SetAlpha(1)
3052 frame:SetFrameStrata("TOOLTIP")
3053 self:Unschedule(testWarningEnd)
3054 self:Schedule(3, testWarningEnd)
3055 frame.timer = 3
3056 end
3057end
3058
3059
3060--------------------
3061-- Timer Object --
3062--------------------
3063do
3064 local timerPrototype = {}
3065 local mt = {__index = timerPrototype}
3066
3067 function timerPrototype:Start(timer, ...)
3068 if timer and type(timer) ~= "number" then
3069 return self:Start(nil, timer, ...) -- first argument is optional!
3070 end
3071 if not self.option or self.mod.Options[self.option] then
3072 local timer = timer and ((timer > 0 and timer) or self.timer + timer) or self.timer
3073 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
3074 local bar = DBM.Bars:CreateBar(timer, id, self.icon)
3075 if not bar then
3076 return false, "error" -- creating the timer failed somehow, maybe hit the hard-coded timer limit of 15
3077 end
3078 if self.type and not self.text then
3079 bar:SetText(pformat(self.mod:GetLocalizedTimerText(self.type, self.spellId), ...))
3080 else
3081 bar:SetText(pformat(self.text, ...))
3082 end
3083 table.insert(self.startedTimers, id)
3084 self.mod:Unschedule(removeEntry, self.startedTimers, id)
3085 self.mod:Schedule(timer, removeEntry, self.startedTimers, id)
3086 return bar
3087 else
3088 return false, "disabled"
3089 end
3090 end
3091 timerPrototype.Show = timerPrototype.Start
3092
3093 function timerPrototype:Schedule(t, ...)
3094 return schedule(t, self.Start, self.mod, self, ...)
3095 end
3096
3097 function timerPrototype:Unschedule(t, ...)
3098 return unschedule(self.Start, self.mod, self, ...)
3099 end
3100
3101 function timerPrototype:Stop(...)
3102 if select("#", ...) == 0 then
3103 for i = #self.startedTimers, 1, -1 do
3104 DBM.Bars:CancelBar(self.startedTimers[i])
3105 self.startedTimers[i] = nil
3106 end
3107 else
3108 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
3109 for i = #self.startedTimers, 1, -1 do
3110 if self.startedTimers[i] == id then
3111 DBM.Bars:CancelBar(id)
3112 table.remove(self.startedTimers, i)
3113 end
3114 end
3115 end
3116 end
3117
3118 function timerPrototype:Cancel(...)
3119 self:Stop(...)
3120 self:Unschedule(...)
3121 end
3122
3123 function timerPrototype:GetTime(...)
3124 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
3125 local bar = DBM.Bars:GetBar(id)
3126 return bar and (bar.totalTime - bar.timer) or 0, (bar and bar.totalTime) or 0
3127 end
3128
3129 function timerPrototype:Time(...)
3130 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
3131 local bar = DBM.Bars:GetBar(id)
3132 return bar.totalTime or 0
3133 end
3134
3135 function timerPrototype:IsStarted(...)
3136 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
3137 local bar = DBM.Bars:GetBar(id)
3138 return bar and true
3139 end
3140
3141 function timerPrototype:SetTimer(timer)
3142 self.timer = timer
3143 end
3144
3145 function timerPrototype:Update(elapsed, totalTime, ...)
3146 if self:GetTime(...) == 0 then
3147 self:Start(totalTime, ...)
3148 end
3149 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
3150 return DBM.Bars:UpdateBar(id, elapsed, totalTime)
3151 end
3152
3153 function timerPrototype:AddTime(time, ...)
3154 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
3155 local timer = self:GetTime(...) - time -- GetTime() = elapsed time on timer
3156 if timer then
3157 return DBM.Bars:UpdateBar(id, timer)
3158 end
3159 end
3160
3161 function timerPrototype:UpdateIcon(icon, ...)
3162 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
3163 local bar = DBM.Bars:GetBar(id)
3164 if bar then
3165 return bar:SetIcon((type(icon) == "number" and select(3, GetSpellInfo(icon))) or icon)
3166 end
3167 end
3168
3169 function timerPrototype:UpdateName(name, ...)
3170 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
3171 local bar = DBM.Bars:GetBar(id)
3172 if bar then
3173 return bar:SetText(name)
3174 end
3175 end
3176
3177 function timerPrototype:SetColor(c, ...)
3178 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
3179 local bar = DBM.Bars:GetBar(id)
3180 if bar then
3181 return bar:SetColor(c)
3182 end
3183 end
3184
3185 function timerPrototype:DisableEnlarge(...)
3186 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
3187 local bar = DBM.Bars:GetBar(id)
3188 if bar then
3189 bar.small = true
3190 end
3191 end
3192
3193 function timerPrototype:AddOption(optionDefault, optionName)
3194 if optionName ~= false then
3195 self.option = optionName or self.id
3196 self.mod:AddBoolOption(self.option, optionDefault, "timer")
3197 end
3198 end
3199
3200 function bossModPrototype:NewTimer(timer, name, icon, optionDefault, optionName, r, g, b)
3201 local icon = type(icon) == "number" and select(3, GetSpellInfo(icon)) or icon
3202 local obj = setmetatable(
3203 {
3204 text = self.localization.timers[name],
3205 timer = timer,
3206 id = name,
3207 icon = icon,
3208 r = r,
3209 g = g,
3210 b = b,
3211 startedTimers = {},
3212 mod = self,
3213 },
3214 mt
3215 )
3216 obj:AddOption(optionDefault, optionName)
3217 table.insert(self.timers, obj)
3218 return obj
3219 end
3220
3221 -- new constructor for the new auto-localized timer types
3222 -- note that the function might look unclear because it needs to handle different timer types, especially achievement timers need special treatment
3223 -- todo: disable the timer if the player already has the achievement and when the ACHIEVEMENT_EARNED event is fired
3224 -- problem: heroic/normal achievements :[
3225 -- local achievementTimers = {}
3226 local function newTimer(self, timerType, timer, spellId, timerText, optionDefault, optionName, texture, r, g, b)
3227 -- new argument timerText is optional (usually only required for achievement timers as they have looooong names)
3228 if type(timerText) == "boolean" or type(optionDefault) == "string" then -- check if the argument was skipped
3229 return newTimer(self, timerType, timer, spellId, nil, timerText, optionDefault, optionName, texture, r, g, b)
3230 end
3231 local spellName, icon
3232 if timerType == "achievement" then
3233 spellName = select(2, GetAchievementInfo(spellId))
3234 icon = type(texture) == "number" and select(10, GetAchievementInfo(texture)) or texture or spellId and select(10, GetAchievementInfo(spellId))
3235-- if optionDefault == nil then
3236-- local completed = select(4, GetAchievementInfo(spellId))
3237-- optionDefault = not completed
3238-- end
3239 else
3240 spellName = GetSpellInfo(spellId or 0)
3241 if spellName then
3242 icon = type(texture) == "number" and select(3, GetSpellInfo(texture)) or texture or spellId and select(3, GetSpellInfo(spellId))
3243 else
3244 icon = nil
3245 end
3246 end
3247 spellName = spellName or tostring(spellId)
3248 local id = "Timer"..(spellId or 0)..self.id..#self.timers
3249 local obj = setmetatable(
3250 {
3251 text = self.localization.timers[timerText],
3252 type = timerType,
3253 spellId = spellId,
3254 timer = timer,
3255 id = id,
3256 icon = icon,
3257 r = r,
3258 g = g,
3259 b = b,
3260 startedTimers = {},
3261 mod = self,
3262 },
3263 mt
3264 )
3265 obj:AddOption(optionDefault, optionName)
3266 table.insert(self.timers, obj)
3267 -- todo: move the string creation to the GUI with SetFormattedString...
3268 if timerType == "achievement" then
3269 self.localization.options[id] = DBM_CORE_AUTO_TIMER_OPTIONS[timerType]:format(GetAchievementLink(spellId):gsub("%[(.+)%]", "%1"))
3270 else
3271 self.localization.options[id] = DBM_CORE_AUTO_TIMER_OPTIONS[timerType]:format(spellId, spellName)
3272 end
3273 return obj
3274 end
3275
3276 function bossModPrototype:NewTargetTimer(...)
3277 return newTimer(self, "target", ...)
3278 end
3279
3280 function bossModPrototype:NewBuffActiveTimer(...)
3281 return newTimer(self, "active", ...)
3282 end
3283
3284 function bossModPrototype:NewCastTimer(timer, ...)
3285 if timer > 1000 then -- hehe :) best hack in DBM. This makes the first argument optional, so we can omit it to use the cast time from the spell id ;)
3286 local spellId = timer
3287 timer = select(7, GetSpellInfo(spellId)) or 1000 -- GetSpellInfo takes YOUR spell haste into account...WTF?
3288 local spellHaste = select(7, GetSpellInfo(53142)) / 10000 -- 53142 = Dalaran Portal, should have 10000 ms cast time
3289 timer = timer / spellHaste -- calculate the real cast time of the spell...
3290 return self:NewCastTimer(timer / 1000, spellId, ...)
3291 end
3292 return newTimer(self, "cast", timer, ...)
3293 end
3294
3295 function bossModPrototype:NewCDTimer(...)
3296 return newTimer(self, "cd", ...)
3297 end
3298
3299 function bossModPrototype:NewNextTimer(...)
3300 return newTimer(self, "next", ...)
3301 end
3302
3303 function bossModPrototype:NewAchievementTimer(...)
3304 return newTimer(self, "achievement", ...)
3305 end
3306
3307 function bossModPrototype:GetLocalizedTimerText(timerType, spellId)
3308 local spellName
3309 if timerType == "achievement" then
3310 spellName = select(2, GetAchievementInfo(spellId))
3311 else
3312 spellName = GetSpellInfo(spellId)
3313 end
3314 return pformat(DBM_CORE_AUTO_TIMER_TEXTS[timerType], spellName)
3315 end
3316end
3317
3318
3319---------------------
3320-- Enrage Object --
3321---------------------
3322do
3323 local enragePrototype = {}
3324 local mt = {__index = enragePrototype}
3325
3326 function enragePrototype:Start(timer)
3327 timer = timer or self.timer or 600
3328 timer = timer <= 0 and self.timer - timer or timer
3329 self.bar:SetTimer(timer)
3330 self.bar:Start()
3331 if timer > 660 then self.warning1:Schedule(timer - 600, 10, DBM_CORE_MIN) end
3332 if timer > 300 then self.warning1:Schedule(timer - 300, 5, DBM_CORE_MIN) end
3333 if timer > 180 then self.warning2:Schedule(timer - 180, 3, DBM_CORE_MIN) end
3334 if timer > 60 then self.warning2:Schedule(timer - 60, 1, DBM_CORE_MIN) end
3335 if timer > 30 then self.warning2:Schedule(timer - 30, 30, DBM_CORE_SEC) end
3336 if timer > 10 then self.warning2:Schedule(timer - 10, 10, DBM_CORE_SEC) end
3337 end
3338
3339 function enragePrototype:Schedule(t)
3340 return self.owner:Schedule(t, self.Start, self)
3341 end
3342
3343 function enragePrototype:Cancel()
3344 self.owner:Unschedule(self.Start, self)
3345 self.warning1:Cancel()
3346 self.warning2:Cancel()
3347 self.bar:Stop()
3348 end
3349 enragePrototype.Stop = enragePrototype.Cancel
3350
3351 function bossModPrototype:NewBerserkTimer(timer, text, barText, barIcon)
3352 timer = timer or 600
3353 local warning1 = self:NewAnnounce(text or DBM_CORE_GENERIC_WARNING_BERSERK, 1, nil, "warning_berserk", false)
3354 local warning2 = self:NewAnnounce(text or DBM_CORE_GENERIC_WARNING_BERSERK, 4, nil, "warning_berserk", false)
3355 local bar = self:NewTimer(timer or 600, barText or DBM_CORE_GENERIC_TIMER_BERSERK, barIcon or 28131, nil, "timer_berserk")
3356 local obj = setmetatable(
3357 {
3358 warning1 = warning1,
3359 warning2 = warning2,
3360 bar = bar,
3361 timer = timer,
3362 owner = self
3363 },
3364 mt
3365 )
3366 return obj
3367 end
3368end
3369
3370
3371---------------
3372-- Options --
3373---------------
3374function bossModPrototype:AddBoolOption(name, default, cat, func)
3375 cat = cat or "misc"
3376 self.Options[name] = (default == nil) or default
3377 self:SetOptionCategory(name, cat)
3378 if func then
3379 self.optionFuncs = self.optionFuncs or {}
3380 self.optionFuncs[name] = func
3381 end
3382end
3383
3384function bossModPrototype:RemoveOption(name)
3385 self.Options[name] = nil
3386 for i, options in pairs(self.optionCategories) do
3387 removeEntry(options, name)
3388 if #options == 0 then
3389 self.optionCategories[i] = nil
3390 removeEntry(self.categorySort, i)
3391 end
3392 end
3393 if self.optionFuncs then
3394 self.optionFuncs[name] = nil
3395 end
3396end
3397
3398function bossModPrototype:AddSliderOption(name, minValue, maxValue, valueStep, default, cat, func)
3399 cat = cat or "misc"
3400 self.Options[name] = default or 0
3401 self:SetOptionCategory(name, cat)
3402 self.sliders = self.sliders or {}
3403 self.sliders[name] = {
3404 minValue = minValue,
3405 maxValue = maxValue,
3406 valueStep = valueStep,
3407 }
3408 if func then
3409 self.optionFuncs = self.optionFuncs or {}
3410 self.optionFuncs[name] = func
3411 end
3412end
3413
3414function bossModPrototype:AddButton(name, onClick, cat, func)
3415 cat = cat or misc
3416 self:SetOptionCategory(name, cat)
3417 self.buttons = self.buttons or {}
3418 self.buttons[name] = onClick
3419 if func then
3420 self.optionFuncs = self.optionFuncs or {}
3421 self.optionFuncs[name] = func
3422 end
3423end
3424
3425function bossModPrototype:AddDropdownOption(name, options, default, cat, func)
3426 cat = cat or "misc"
3427 self.Options[name] = default
3428 self:SetOptionCategory(name, cat)
3429 self.dropdowns = self.dropdowns or {}
3430 self.dropdowns[name] = options
3431 if func then
3432 self.optionFuncs = self.optionFuncs or {}
3433 self.optionFuncs[name] = func
3434 end
3435end
3436
3437function bossModPrototype:AddOptionSpacer(cat)
3438 cat = cat or "misc"
3439 if self.optionCategories[cat] then
3440 table.insert(self.optionCategories[cat], DBM_OPTION_SPACER)
3441 end
3442end
3443
3444function bossModPrototype:AddAnnounceSpacer()
3445 return self:AddOptionSpacer("announce")
3446end
3447
3448function bossModPrototype:AddTimerSpacer()
3449 return self:AddOptionSpacer("timer")
3450end
3451
3452
3453function bossModPrototype:SetOptionCategory(name, cat)
3454 for _, options in pairs(self.optionCategories) do
3455 removeEntry(options, name)
3456 end
3457 if not self.optionCategories[cat] then
3458 self.optionCategories[cat] = {}
3459 table.insert(self.categorySort, cat)
3460 end
3461 table.insert(self.optionCategories[cat], name)
3462end
3463
3464
3465--------------
3466-- Combat --
3467--------------
3468function bossModPrototype:RegisterCombat(cType, ...)
3469 if cType then
3470 cType = cType:lower()
3471 end
3472 local info = {
3473 type = cType,
3474 mob = self.creatureId,
3475 name = self.localization.general.name or self.id,
3476 msgs = (cType ~= "combat") and {...},
3477 mod = self
3478 }
3479 if self.multiMobPullDetection then
3480 info.multiMobPullDetection = self.multiMobPullDetection
3481 end
3482 local addedKillMobs = false
3483 for i = 1, select("#", ...) do
3484 local v = select(i, ...)
3485 if type(v) == "number" then
3486 info.killMobs = info.killMobs or {}
3487 info.killMobs[select(i, ...)] = true
3488 addedKillMobs = true
3489 end
3490 end
3491 if not addedKillMobs and self.multiMobPullDetection then
3492 for i, v in ipairs(self.multiMobPullDetection) do
3493 info.killMobs = info.killMobs or {}
3494 info.killMobs[v] = true
3495 end
3496 end
3497 self.combatInfo = info
3498 if not self.zones then return end
3499 for i, v in ipairs(self.zones) do
3500 combatInfo[v] = combatInfo[v] or {}
3501 table.insert(combatInfo[v], info)
3502 end
3503end
3504
3505-- needs to be called _AFTER_ RegisterCombat
3506function bossModPrototype:RegisterKill(msgType, ...)
3507 if cType then
3508 cType = cType:lower()
3509 end
3510 if not self.combatInfo then
3511 return
3512 end
3513 self.combatInfo.killType = msgType
3514 self.combatInfo.killMsgs = {}
3515 for i = 1, select("#", ...) do
3516 local v = select(i, ...)
3517 self.combatInfo.killMsgs[v] = true
3518 end
3519end
3520
3521-- needs to be called _AFTER_ RegisterCombat
3522function bossModPrototype:SetDetectCombatInVehicle(flag)
3523 if not self.combatInfo then
3524 return
3525 end
3526 self.combatInfo.noCombatInVehicle = not flag
3527end
3528
3529function bossModPrototype:IsInCombat()
3530 return self.inCombat
3531end
3532
3533function bossModPrototype:SetMinCombatTime(t)
3534 self.minCombatTime = t
3535end
3536
3537-- needs to be called after RegisterCombat
3538function bossModPrototype:SetWipeTime(t)
3539 self.combatInfo.wipeTimer = t
3540end
3541
3542function bossModPrototype:GetBossHPString(cId)
3543 local idType = (GetNumRaidMembers() == 0 and "party") or "raid"
3544 for i = 0, math.max(GetNumRaidMembers(), GetNumPartyMembers()) do
3545 local unitId = ((i == 0) and "target") or idType..i.."target"
3546 local guid = UnitGUID(unitId)
3547 if guid and tonumber(guid:sub(9, 12), 16) == cId then
3548 return math.floor(UnitHealth(unitId)/UnitHealthMax(unitId) * 100).."%"
3549 end
3550 end
3551 return DBM_CORE_UNKNOWN
3552end
3553
3554function bossModPrototype:GetHP()
3555 return self:GetBossHPString((self.combatInfo and self.combatInfo.mob) or self.creatureId)
3556end
3557
3558function bossModPrototype:IsWipe()
3559 local wipe = true
3560 local uId = ((GetNumRaidMembers() == 0) and "party") or "raid"
3561 for i = 0, math.max(GetNumRaidMembers(), GetNumPartyMembers()) do
3562 local id = (i == 0 and "player") or uId..i
3563 if UnitAffectingCombat(id) and not UnitIsDeadOrGhost(id) then
3564 wipe = false
3565 break
3566 end
3567 end
3568 return wipe
3569end
3570
3571
3572
3573-----------------------
3574-- Synchronization --
3575-----------------------
3576function bossModPrototype:SendSync(event, arg)
3577 event = event or ""
3578 arg = arg or ""
3579 local str = ("%s\t%s\t%s\t%s"):format(self.id, self.revision or 0, event, arg)
3580 local spamId = self.id..event..arg
3581 local time = GetTime()
3582 if not modSyncSpam[spamId] or (time - modSyncSpam[spamId]) > 2.5 then
3583 self:ReceiveSync(event, arg, nil, self.revision or 0)
3584 sendSync("DBMv4-Mod", str)
3585 end
3586end
3587
3588function bossModPrototype:ReceiveSync(event, arg, sender, revision)
3589 local spamId = self.id..event..arg
3590 local time = GetTime()
3591 if (not modSyncSpam[spamId] or (time - modSyncSpam[spamId]) > 2.5) and self.OnSync and (not (self.blockSyncs and sender)) and (not sender or (not self.minSyncRevision or revision >= self.minSyncRevision)) then
3592 modSyncSpam[spamId] = time
3593 self:OnSync(event, arg, sender)
3594 end
3595end
3596
3597function bossModPrototype:SetMinSyncRevision(revision)
3598 self.minSyncRevision = revision
3599end
3600
3601
3602-----------------
3603-- Scheduler --
3604-----------------
3605function bossModPrototype:Schedule(t, f, ...)
3606 return schedule(t, f, self, ...)
3607end
3608
3609function bossModPrototype:Unschedule(f, ...)
3610 return unschedule(f, self, ...)
3611end
3612
3613function bossModPrototype:ScheduleMethod(t, method, ...)
3614 if not self[method] then
3615 error(("Method %s does not exist"):format(tostring(method)), 2)
3616 end
3617 return self:Schedule(t, self[method], self, ...)
3618end
3619bossModPrototype.ScheduleEvent = bossModPrototype.ScheduleMethod
3620
3621function bossModPrototype:UnscheduleMethod(method, ...)
3622 if not self[method] then
3623 error(("Method %s does not exist"):format(tostring(method)), 2)
3624 end
3625 return self:Unschedule(self[method], self, ...)
3626end
3627bossModPrototype.UnscheduleEvent = bossModPrototype.UnscheduleMethod
3628
3629
3630-------------
3631-- Icons --
3632-------------
3633function bossModPrototype:SetIcon(target, icon, timer)
3634 if DBM.Options.DontSetIcons or not enableIcons or DBM:GetRaidRank() == 0 then
3635 return
3636 end
3637 icon = icon and icon >= 0 and icon <= 8 and icon or 8
3638 local oldIcon = self:GetIcon(target) or 0
3639 SetRaidTarget(DBM:GetRaidUnitId(target), icon)
3640 self:UnscheduleMethod("SetIcon", target)
3641 if timer then
3642 self:ScheduleMethod(timer, "RemoveIcon", target)
3643 if oldIcon then
3644 self:ScheduleMethod(timer + 1, "SetIcon", target, oldIcon)
3645 end
3646 end
3647end
3648
3649function bossModPrototype:GetIcon(target)
3650 return GetRaidTargetIndex(DBM:GetRaidUnitId(target))
3651end
3652
3653function bossModPrototype:RemoveIcon(target, timer)
3654 return self:SetIcon(target, 0, timer)
3655end
3656
3657function bossModPrototype:ClearIcons()
3658 if GetNumRaidMembers() > 0 then
3659 for i = 1, GetNumRaidMembers() do
3660 if UnitExists("raid"..i) and GetRaidTargetIndex("raid"..i) then
3661 SetRaidTarget("raid"..i, 0)
3662 end
3663 end
3664 else
3665 for i = 1, GetNumPartyMembers() do
3666 if UnitExists("party"..i) and GetRaidTargetIndex("party"..i) then
3667 SetRaidTarget("party"..i, 0)
3668 end
3669 end
3670 end
3671end
3672
3673-----------------------
3674-- Model Functions --
3675-----------------------
3676function bossModPrototype:SetModelScale(scale)
3677 self.modelScale = scale
3678end
3679
3680function bossModPrototype:SetModelOffset(x, y, z)
3681 self.modelOffsetX = x
3682 self.modelOffsetY = y
3683 self.modelOffsetZ = z
3684end
3685
3686function bossModPrototype:SetModelRotation(r)
3687 self.modelRotation = r
3688end
3689
3690function bossModPrototype:SetModelMoveSpeed(v)
3691 self.modelMoveSpeed = v
3692end
3693
3694function bossModPrototype:SetModelID(id)
3695 self.modelId = id
3696end
3697
3698function bossModPrototype:EnableModel()
3699 self.modelEnabled = true
3700end
3701
3702function bossModPrototype:DisableModel()
3703 self.modelEnabled = nil
3704end
3705
3706
3707--------------------
3708-- Localization --
3709--------------------
3710function bossModPrototype:GetLocalizedStrings()
3711 return self.localization.miscStrings
3712end
3713
3714-- Not really good, needs a few updates
3715do
3716 local modLocalizations = {}
3717 local modLocalizationPrototype = {}
3718 local mt = {__index = modLocalizationPrototype}
3719 local returnKey = {__index = function(t, k) return k end}
3720 local defaultCatLocalization = {
3721 __index = setmetatable({
3722 timer = DBM_CORE_OPTION_CATEGORY_TIMERS,
3723 announce = DBM_CORE_OPTION_CATEGORY_WARNINGS,
3724 misc = DBM_CORE_OPTION_CATEGORY_MISC
3725 }, returnKey)
3726 }
3727 local defaultTimerLocalization = {
3728 __index = setmetatable({
3729 timer_berserk = DBM_CORE_GENERIC_TIMER_BERSERK,
3730 TimerSpeedKill = DBM_CORE_ACHIEVEMENT_TIMER_SPEED_KILL
3731 }, returnKey)
3732 }
3733 local defaultAnnounceLocalization = {
3734 __index = setmetatable({
3735 warning_berserk = DBM_CORE_GENERIC_WARNING_BERSERK
3736 }, returnKey)
3737 }
3738 local defaultOptionLocalization = {
3739 __index = setmetatable({
3740 timer_berserk = DBM_CORE_OPTION_TIMER_BERSERK,
3741 HealthFrame = DBM_CORE_OPTION_HEALTH_FRAME
3742 }, returnKey)
3743 }
3744 local defaultMiscLocalization = {
3745 __index = function(t, k)
3746 return t.misc.general[k] or t.misc.options[k] or t.misc.warnings[k] or t.misc.timers[k] or t.misc.cats[k] or k
3747 end
3748 }
3749
3750 function modLocalizationPrototype:SetGeneralLocalization(t)
3751 for i, v in pairs(t) do
3752 self.general[i] = v
3753 end
3754 end
3755
3756 function modLocalizationPrototype:SetWarningLocalization(t)
3757 for i, v in pairs(t) do
3758 self.warnings[i] = v
3759 end
3760 end
3761
3762 function modLocalizationPrototype:SetTimerLocalization(t)
3763 for i, v in pairs(t) do
3764 self.timers[i] = v
3765 end
3766 end
3767
3768 function modLocalizationPrototype:SetOptionLocalization(t)
3769 for i, v in pairs(t) do
3770 self.options[i] = v
3771 end
3772 end
3773
3774 function modLocalizationPrototype:SetOptionCatLocalization(t)
3775 for i, v in pairs(t) do
3776 self.cats[i] = v
3777 end
3778 end
3779
3780 function modLocalizationPrototype:SetMiscLocalization(t)
3781 for i, v in pairs(t) do
3782 self.miscStrings[i] = v
3783 end
3784 end
3785
3786 function DBM:CreateModLocalization(name)
3787 local obj = {
3788 general = setmetatable({}, returnKey),
3789 warnings = setmetatable({}, defaultAnnounceLocalization),
3790 options = setmetatable({}, defaultOptionLocalization),
3791 timers = setmetatable({}, defaultTimerLocalization),
3792 miscStrings = setmetatable({}, defaultMiscLocalization),
3793 cats = setmetatable({}, defaultCatLocalization),
3794 }
3795 obj.miscStrings.misc = obj
3796 setmetatable(obj, mt)
3797 modLocalizations[name] = obj
3798 return obj
3799 end
3800
3801 function DBM:GetModLocalization(name)
3802 return modLocalizations[name] or self:CreateModLocalization(name)
3803 end
3804end