Files
coa-dbm/DBM-Core/DBM-Core.lua
T
2026-05-29 19:27:50 +02:00

4051 lines
128 KiB
Lua

-- *********************************************************
-- ** Deadly Boss Mods - Core **
-- ** http://www.deadlybossmods.com **
-- *********************************************************
--
-- This addon is written and copyrighted by:
-- * Paul Emmerich (Tandanu @ EU-Aegwynn) (DBM-Core)
-- * Martin Verges (Nitram @ EU-Azshara) (DBM-GUI)
--
-- The localizations are written by:
-- * enGB/enUS: Tandanu http://www.deadlybossmods.com
-- * deDE: Tandanu http://www.deadlybossmods.com
-- * zhCN: Diablohu http://wow.gamespot.com.cn
-- * ruRU: BootWin bootwin@gmail.com
-- * ruRU: Vampik admin@vampik.ru
-- * zhTW: Hman herman_c1@hotmail.com
-- * zhTW: Azael/kc10577 kc10577@hotmail.com
-- * koKR: BlueNyx bluenyx@gmail.com
-- * esES: Interplay/1nn7erpLaY http://www.1nn7erpLaY.com
--
-- Special thanks to:
-- * Arta (DBM-Party)
-- * Omegal @ US-Whisperwind (continuing mod support for 3.2+)
-- * Tennberg (a lot of fixes in the enGB/enUS localization)
--
--
-- The code of this addon is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 License. (see license.txt)
-- 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.
--
--
-- You are free:
-- * to Share - to copy, distribute, display, and perform the work
-- * to Remix - to make derivative works
-- Under the following conditions:
-- * 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)
-- * Noncommercial. You may not use this work for commercial purposes.
-- * 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.
--
-------------------------------
-- Globals/Default Options --
-------------------------------
DBM = {
Revision = ("$Revision: 5021 $"):sub(12, -3),
Version = "5.21",
DisplayVersion = "5.21", -- the string that is shown as version
ReleaseRevision = 5020 -- the revision of the latest stable version that is available (for /dbm ver2)
}
DBM_SavedOptions = {}
DBM.DefaultOptions = {
WarningColors = {
{r = 0.41, g = 0.80, b = 0.94}, -- Color 1 - #69CCF0 - Turqoise
{r = 0.95, g = 0.95, b = 0.00}, -- Color 2 - #F2F200 - Yellow
{r = 1.00, g = 0.50, b = 0.00}, -- Color 3 - #FF8000 - Orange
{r = 1.00, g = 0.10, b = 0.10}, -- Color 4 - #FF1A1A - Red
},
RaidWarningSound = "Sound\\Doodad\\BellTollNightElf.wav",
SpecialWarningSound = "Sound\\Spells\\PVPFlagTaken.wav",
RaidWarningPosition = {
Point = "TOP",
X = 0,
Y = -185,
},
StatusEnabled = true,
AutoRespond = true,
Enabled = true,
ShowWarningsInChat = true,
ShowFakedRaidWarnings = false,
WarningIconLeft = true,
WarningIconRight = true,
HideBossEmoteFrame = false,
SpamBlockRaidWarning = true,
SpamBlockBossWhispers = false,
ShowMinimapButton = true,
FixCLEUOnCombatStart = false,
BlockVersionUpdatePopup = true,
ShowSpecialWarnings = true,
AlwaysShowHealthFrame = false,
ShowBigBrotherOnCombatStart = false,
RangeFramePoint = "CENTER",
RangeFrameX = 50,
RangeFrameY = -50,
RangeFrameSound1 = "none",
RangeFrameSound2 = "none",
RangeFrameLocked = false,
HPFramePoint = "CENTER",
HPFrameX = -50,
HPFrameY = 50,
HPFrameMaxEntries = 5,
SpecialWarningPoint = "CENTER",
SpecialWarningX = 0,
SpecialWarningY = 75,
SpecialWarningFont = STANDARD_TEXT_FONT,
SpecialWarningFontSize = 50,
SpecialWarningFontColor = {0.0, 0.0, 1.0},
HealthFrameGrowUp = false,
HealthFrameLocked = false,
HealthFrameWidth = 200,
ArrowPosX = 0,
ArrowPosY = -150,
ArrowPoint = "TOP",
-- global boss mod settings (overrides mod-specific settings for some options)
DontShowBossAnnounces = false,
DontSendBossAnnounces = false,
DontSendBossWhispers = false,
DontSetIcons = false,
LatencyThreshold = 250,
BigBrotherAnnounceToRaid = false,
-- HelpMessageShown = false,
}
DBM.BarGroups = {};
function DBM:CreateBarGroup(id)
local barGroup = DBT:New();
DBM.BarGroups[id] = barGroup;
return barGroup;
end
DBM.Bars = DBM:CreateBarGroup("DBM");
function DBM:GetBarGroup(id)
return DBM.BarGroups[id] or DBM.Bars;
end
DBM.Mods = {}
------------------------
-- Global Identifiers --
------------------------
DBM_DISABLE_ZONE_DETECTION = newproxy(false)
DBM_OPTION_SPACER = newproxy(false)
--------------
-- Locals --
--------------
local inCombat = {}
local combatInfo = {}
local updateFunctions = {}
local raid = {}
local modSyncSpam = {}
local autoRespondSpam = {}
local chatPrefix = "<Deadly Boss Mods> "
local chatPrefixShort = "<DBM> "
local ver = ("%s (r%d)"):format(DBM.DisplayVersion, DBM.Revision)
local mainFrame = CreateFrame("Frame")
local showedUpdateReminder = false
local combatInitialized = false
local schedule
local unschedule
local loadOptions
local loadModOptions
local checkWipe
local fireEvent
local wowVersion = select(4, GetBuildInfo())
local enableIcons = true -- set to false when a raid leader or a promoted player has a newer version of DBM
local 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)
"DBM-Battlegrounds", --replaced by DBM-PvP
}
--------------------------------------------------------
-- Cache frequently used global variables in locals --
--------------------------------------------------------
local DBM = DBM
-- these global functions are accessed all the time by the event handler
-- so caching them is worth the effort
local ipairs, pairs, next = ipairs, pairs, next
local tinsert, tremove, twipe = table.insert, table.remove, table.wipe
local type = type
local select = select
local floor = math.floor
-- for Phanx' Class Colors
local RAID_CLASS_COLORS = CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS
---------------------------------
-- General (local) functions --
---------------------------------
-- checks if a given value is in an array
-- returns true if it finds the value, false otherwise
local function checkEntry(t, val)
for i, v in ipairs(t) do
if v == val then
return true
end
end
return false
end
-- removes all occurrences of a value in an array
-- returns true if at least one occurrence was remove, false otherwise
local function removeEntry(t, val)
local existed = false
for i = #t, 1, -1 do
if t[i] == val then
table.remove(t, i)
existed = true
end
end
return existed
end
-- automatically sends an addon message to the appropriate channel (BATTLEGROUND, RAID or PARTY)
local function sendSync(prefix, msg)
local zoneType = select(2, IsInInstance())
if zoneType == "pvp" or zoneType == "arena" then
SendAddonMessage(prefix, msg, "BATTLEGROUND")
elseif GetRealNumRaidMembers() > 0 then
SendAddonMessage(prefix, msg, "RAID")
elseif GetRealNumPartyMembers() > 0 then
SendAddonMessage(prefix, msg, "PARTY")
end
end
--
local function strFromTime(time)
if type(time) ~= "number" then time = 0 end
time = math.floor(time)
if time < 60 then
return DBM_CORE_TIMER_FORMAT_SECS:format(time)
elseif time % 60 == 0 then
return DBM_CORE_TIMER_FORMAT_MINS:format(time/60)
else
return DBM_CORE_TIMER_FORMAT:format(time/60, time % 60)
end
end
local pformat
do
-- fail-safe format, replaces missing arguments with unknown
-- note: doesn't handle cases like %%%s correctly at the moment (should become %unknown, but becomes %%s)
-- also, the end of the format directive is not detected in all cases, but handles everything that occurs in our boss mods ;)
--> 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)
local function replace(cap1, cap2)
return cap1 == "%" and DBM_CORE_UNKNOWN
end
function pformat(fstr, ...)
local ok, str = pcall(format, fstr, ...)
return ok and str or fstr:gsub("(%%+)([^%%%s<]+)", replace):gsub("%%%%", "%%")
end
end
-- sends a whisper to a player by his or her character name or BNet presence id
-- returns true if the message was sent, nil otherwise
local function sendWhisper(target, msg)
if type(target) == "number" then
if not BNIsSelf(target) then -- never send BNet whispers to ourselves
BNSendWhisper(target, msg)
return true
end
elseif type(target) == "string" then
-- whispering to ourselves here is okay and somewhat useful for whisper-warnings
SendChatMessage(msg, "WHISPER", nil, target)
return true
end
end
local BNSendWhisper = sendWhisper
--------------
-- Events --
--------------
do
local registeredEvents = {}
local argsMT = {__index = {}}
local args = setmetatable({}, argsMT)
function argsMT.__index:IsSpellID(a1, a2, a3, a4)
local v = self.spellId
return v == a1 or v == a2 or v == a3 or v == a4
end
function argsMT.__index:IsPlayer()
return bit.band(args.destFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) ~= 0 and bit.band(args.destFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
end
function argsMT.__index:IsPlayerSource()
return bit.band(args.sourceFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) ~= 0 and bit.band(args.sourceFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
end
function argsMT.__index:IsPet()
return bit.band(args.destFlags, COMBATLOG_OBJECT_TYPE_PET) ~= 0
end
function argsMT.__index:IsPetSource()
return bit.band(args.sourceFlags, COMBATLOG_OBJECT_TYPE_PET) ~= 0
end
function argsMT.__index:IsSrcTypePlayer()
return bit.band(args.sourceFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
end
function argsMT.__index:IsDestTypePlayer()
return bit.band(args.destFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
end
function argsMT.__index:IsSrcTypeHostile()
return bit.band(args.sourceFlags, COMBATLOG_OBJECT_REACTION_HOSTILE) ~= 0
end
function argsMT.__index:IsDestTypeHostile()
return bit.band(args.destFlags, COMBATLOG_OBJECT_REACTION_HOSTILE) ~= 0
end
function argsMT.__index:GetSrcCreatureID()
return tonumber(self.sourceGUID:sub(9, 12), 16) or 0
end
function argsMT.__index:GetDestCreatureID()
return tonumber(self.destGUID:sub(9, 12), 16) or 0
end
local function handleEvent(self, event, ...)
if not registeredEvents[event] or DBM.Options and not DBM.Options.Enabled then return end
for i, v in ipairs(registeredEvents[event]) do
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
v[event](v, ...)
end
end
end
function DBM:RegisterEvents(...)
for i = 1, select("#", ...) do
local ev = select(i, ...)
registeredEvents[ev] = registeredEvents[ev] or {}
tinsert(registeredEvents[ev], self)
mainFrame:RegisterEvent(ev)
end
end
function DBM:UnregisterAllEvents()
for i, v in pairs(registeredEvents) do
for i = #v, 1 do
if v[i] == self then
tremove(v, i)
end
end
if #v == 0 then
registeredEvents[i] = nil
mainFrame:UnregisterEvent(i)
end
end
end
DBM:RegisterEvents("ADDON_LOADED")
function DBM:FilterRaidBossEmote(msg, ...)
return handleEvent(nil, "CHAT_MSG_RAID_BOSS_EMOTE_FILTERED", msg:gsub("\124c%x+(.-)\124r", "%1"), ...)
end
function DBM:COMBAT_LOG_EVENT_UNFILTERED(timestamp, event, sourceGUID, sourceName, sourceFlags, destGUID, destName, destFlags, ...)
if not registeredEvents[event] then return end
twipe(args)
args.timestamp = timestamp
args.event = event
args.sourceGUID = sourceGUID
args.sourceName = sourceName
args.sourceFlags = sourceFlags
args.destGUID = destGUID
args.destName = destName
args.destFlags = destFlags
-- taken from Blizzard_CombatLog.lua
if event == "SWING_DAMAGE" then
args.amount, args.overkill, args.school, args.resisted, args.blocked, args.absorbed, args.critical, args.glancing, args.crushing = select(1, ...)
elseif event == "SWING_MISSED" then
args.spellName = ACTION_SWING
args.missType = select(1, ...)
elseif event:sub(1, 5) == "RANGE" then
args.spellId, args.spellName, args.spellSchool = select(1, ...)
if event == "RANGE_DAMAGE" then
args.amount, args.overkill, args.school, args.resisted, args.blocked, args.absorbed, args.critical, args.glancing, args.crushing = select(4, ...)
elseif event == "RANGE_MISSED" then
args.missType = select(4, ...)
end
elseif event:sub(1, 5) == "SPELL" then
args.spellId, args.spellName, args.spellSchool = select(1, ...)
if event == "SPELL_DAMAGE" then
args.amount, args.overkill, args.school, args.resisted, args.blocked, args.absorbed, args.critical, args.glancing, args.crushing = select(4, ...)
elseif event == "SPELL_MISSED" then
args.missType, args.amountMissed = select(4, ...)
elseif event == "SPELL_HEAL" then
args.amount, args.overheal, args.absorbed, args.critical = select(4, ...)
args.school = args.spellSchool
elseif event == "SPELL_ENERGIZE" then
args.valueType = 2
args.amount, args.powerType = select(4, ...)
elseif event:sub(1, 14) == "SPELL_PERIODIC" then
if event == "SPELL_PERIODIC_MISSED" then
args.missType = select(4, ...)
elseif event == "SPELL_PERIODIC_DAMAGE" then
args.amount, args.overkill, args.school, args.resisted, args.blocked, args.absorbed, args.critical, args.glancing, args.crushing = select(4, ...)
elseif event == "SPELL_PERIODIC_HEAL" then
args.amount, args.overheal, args.absorbed, args.critical = select(4, ...)
args.school = args.spellSchool
elseif event == "SPELL_PERIODIC_DRAIN" then
args.amount, args.powerType, args.extraAmount = select(4, ...)
args.valueType = 2
elseif event == "SPELL_PERIODIC_LEECH" then
args.amount, args.powerType, args.extraAmount = select(4, ...)
args.valueType = 2
elseif event == "SPELL_PERIODIC_ENERGIZE" then
args.amount, args.powerType = select(4, ...)
args.valueType = 2
end
elseif event == "SPELL_DRAIN" then
args.amount, args.powerType, args.extraAmount = select(4, ...)
args.valueType = 2
elseif event == "SPELL_LEECH" then
args.amount, args.powerType, args.extraAmount = select(4, ...)
args.valueType = 2
elseif event == "SPELL_INTERRUPT" then
args.extraSpellId, args.extraSpellName, args.extraSpellSchool = select(4, ...)
elseif event == "SPELL_EXTRA_ATTACKS" then
args.amount = select(4, ...)
elseif event == "SPELL_DISPEL_FAILED" then
args.extraSpellId, args.extraSpellName, args.extraSpellSchool = select(4, ...)
elseif event == "SPELL_AURA_DISPELLED" then
args.extraSpellId, args.extraSpellName, args.extraSpellSchool = select(4, ...)
args.auraType = select(7, ...)
elseif event == "SPELL_AURA_STOLEN" then
args.extraSpellId, args.extraSpellName, args.extraSpellSchool = select(4, ...)
args.auraType = select(7, ...)
elseif event == "SPELL_AURA_APPLIED" or event == "SPELL_AURA_REMOVED" then
args.auraType = select(4, ...)
args.sourceName = args.destName
args.sourceGUID = args.destGUID
args.sourceFlags = args.destFlags
elseif event == "SPELL_AURA_APPLIED_DOSE" or event == "SPELL_AURA_REMOVED_DOSE" then
args.auraType, args.amount = select(4, ...)
args.sourceName = args.destName
args.sourceGUID = args.destGUID
args.sourceFlags = args.destFlags
elseif event == "SPELL_CAST_FAILED" then
args.missType = select(4, ...)
end
elseif event == "DAMAGE_SHIELD" then
args.spellId, args.spellName, args.spellSchool = select(1, ...)
args.amount, args.school, args.resisted, args.blocked, args.absorbed, args.critical, args.glancing, args.crushing = select(4, ...)
elseif event == "DAMAGE_SHIELD_MISSED" then
args.spellId, args.spellName, args.spellSchool = select(1, ...)
args.missType = select(4, ...)
elseif event == "ENCHANT_APPLIED" then
args.spellName = select(1,...)
args.itemId, args.itemName = select(2,...)
elseif event == "ENCHANT_REMOVED" then
args.spellName = select(1,...)
args.itemId, args.itemName = select(2,...)
elseif event == "UNIT_DIED" or event == "UNIT_DESTROYED" then
args.sourceName = args.destName
args.sourceGUID = args.destGUID
args.sourceFlags = args.destFlags
elseif event == "ENVIRONMENTAL_DAMAGE" then
args.environmentalType = select(1,...)
args.amount, args.overkill, args.school, args.resisted, args.blocked, args.absorbed, args.critical, args.glancing, args.crushing = select(2, ...)
args.spellName = _G["ACTION_"..event.."_"..args.environmentalType]
args.spellSchool = args.school
elseif event == "DAMAGE_SPLIT" then
args.spellId, args.spellName, args.spellSchool = select(1, ...)
args.amount, args.school, args.resisted, args.blocked, args.absorbed, args.critical, args.glancing, args.crushing = select(4, ...)
end
return handleEvent(nil, event, args)
end
mainFrame:SetScript("OnEvent", handleEvent)
end
-----------------
-- Callbacks --
-----------------
do
local callbacks = {}
function fireEvent(event, ...)
if not callbacks[event] then return end
for i, v in ipairs(callbacks[event]) do
local ok, err = pcall(v, event, ...)
if not ok then DBM:AddMsg(("Error while executing callback %s for event %s: %s"):format(tostring(v), tostring(event), err)) end
end
end
function DBM:IsCallbackRegistered(event, f)
if not event or type(f) ~= "function" then
error("Usage: IsCallbackRegistered(event, callbackFunc)", 2)
end
if not callbacks[event] then return end
for i = 1, #callbacks[event] do
if callbacks[event][i] == f then return true end
end
return false
end
function DBM:RegisterCallback(event, f)
if not event or type(f) ~= "function" then
error("Usage: DBM:RegisterCallback(event, callbackFunc)", 2)
end
callbacks[event] = callbacks[event] or {}
table.insert(callbacks[event], f)
return #callbacks[event]
end
end
--------------------------
-- OnUpdate/Scheduler --
--------------------------
do
-- stack that stores a few tables (up to 8) which will be recycled
local popCachedTable, pushCachedTable
local numChachedTables = 0
do
local tableCache = nil
-- gets a table from the stack, it will then be recycled.
function popCachedTable()
local t = tableCache
if t then
tableCache = t.next
numChachedTables = numChachedTables - 1
end
return t
end
-- tries to push a table on the stack
-- only tables with <= 4 array entries are accepted as cached tables are only used for tasks with few arguments for performance reasons
-- 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
-- this is just to re-use all the tables of the small tasks that are scheduled all the time (like the wipe detection)
-- 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
function pushCachedTable(t)
if numChachedTables < 8 and #t <= 4 then
twipe(t)
t.next = tableCache
tableCache = t
numChachedTables = numChachedTables + 1
end
end
end
-- priority queue (min-heap) that stores all scheduled tasks.
-- insert: O(log n)
-- deleteMin: O(log n)
-- getMin: O(1)
-- removeAllMatching: O(n)
local insert, removeAllMatching, getMin, deleteMin
do
local heap = {}
local firstFree = 1
-- gets the next task
function getMin()
return heap[1]
end
-- restores the heap invariant by moving an item up
local function siftUp(n)
local parent = floor(n / 2)
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
heap[n], heap[parent] = heap[parent], heap[n] -- swap the element with its parent
n = parent
parent = floor(n / 2)
end
end
-- restores the heap invariant by moving an item down
local function siftDown(n)
local m -- position of the smaller child
while 2 * n < firstFree do -- #children >= 1
-- swap the element with its smaller child
if 2 * n + 1 == firstFree then -- n does not have a right child --> it only has a left child as #children >= 1
m = 2 * n -- left child
elseif heap[2 * n].time < heap[2 * n + 1].time then -- #children = 2 and left child < right child
m = 2 * n -- left child
else -- #children = 2 and right child is smaller than the left one
m = 2 * n + 1 -- right
end
if heap[n].time <= heap[m].time then -- n is <= its smallest child --> heap invariant restored
return
end
heap[n], heap[m] = heap[m], heap[n]
n = m
end
end
-- inserts a new element into the heap
function insert(ele)
heap[firstFree] = ele
siftUp(firstFree)
firstFree = firstFree + 1
end
-- deletes the min element
function deleteMin()
local min = heap[1]
firstFree = firstFree - 1
heap[1] = heap[firstFree]
heap[firstFree] = nil
siftDown(1)
return min
end
-- removes multiple scheduled tasks from the heap
-- note that this function is comparatively slow by design as it has to check all tasks and allows partial matches
function removeAllMatching(f, mod, ...)
-- remove all elements that match the signature, this destroyes the heap and leaves a normal array
local v, match
for i = #heap, 1, -1 do -- iterate backwards over the array to allow usage of table.remove
v = heap[i]
if (not f or v.func == f) and (not mod or v.mod == mod) then
match = true
for i = 1, select("#", ...) do
if select(i, ...) ~= v[i] then
match = false
break
end
end
if match then
table.remove(heap, i)
firstFree = firstFree - 1
end
end
end
-- rebuild the heap from the array in O(n)
for i = floor((firstFree - 1) / 2), 1, -1 do
siftDown(i)
end
end
end
mainFrame:SetScript("OnUpdate", function(self, elapsed)
local time = GetTime()
-- execute scheduled tasks
local nextTask = getMin()
while nextTask and nextTask.time <= time do
deleteMin()
nextTask.func(unpack(nextTask))
pushCachedTable(nextTask)
nextTask = getMin()
end
-- execute OnUpdate handlers of all modules
for i, v in pairs(updateFunctions) do
if i.Options.Enabled and (not i.zones or checkEntry(i.zones, GetRealZoneText()) or checkEntry(i.zones, GetCurrentMapAreaID())) then
i.elapsed = (i.elapsed or 0) + elapsed
if i.elapsed >= (i.updateInterval or 0) then
v(i, i.elapsed)
i.elapsed = 0
end
end
end
-- clean up sync spam timers and auto respond spam blockers
-- 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...
local k, v = next(modSyncSpam, nil)
if v and (time - v > 2.5) then
modSyncSpam[k] = nil
end
end)
function schedule(t, f, mod, ...)
local v
if numChachedTables > 0 and select("#", ...) <= 4 then -- a cached table is available and all arguments fit into an array with four slots
v = popCachedTable()
v.time = GetTime() + t
v.func = f
v.mod = mod
for i = 1, select("#", ...) do
v[i] = select(i, ...)
end
else -- create a new table
v = {time = GetTime() + t, func = f, mod = mod, ...}
end
insert(v)
end
function scheduleCountdown(time, numAnnounces, func, mod, self, ...)
time = time or 5
numAnnounces = numAnnounces or 3
for i = 1, numAnnounces do
schedule(time - i, func, mod, self, i, ...)
end
end
function unschedule(f, mod, ...)
return removeAllMatching(f, mod, ...)
end
end
function DBM:Schedule(t, f, ...)
return schedule(t, f, nil, ...)
end
function DBM:Unschedule(f, ...)
return unschedule(f, nil, ...)
end
function DBM:ForceUpdate()
mainFrame:GetScript("OnUpdate")(mainFrame, 0)
end
----------------------
-- Slash Commands --
----------------------
SLASH_DEADLYBOSSMODS1 = "/dbm"
SLASH_PULL1 = "/pull"
SlashCmdList["DEADLYBOSSMODS"] = function(msg)
local cmd = msg:lower()
if cmd == "ver" or cmd == "version" then
DBM:ShowVersions(false)
elseif cmd == "ver2" or cmd == "version2" then
DBM:ShowVersions(true)
elseif cmd == "unlock" or cmd == "move" then
for id,barGroup in pairs(DBM.BarGroups) do
barGroup:ShowMovableBar()
end
elseif cmd == "help" then
for i, v in ipairs(DBM_CORE_SLASHCMD_HELP) do DBM:AddMsg(v) end
elseif cmd:sub(1, 5) == "timer" then
local time, text = msg:match("^%w+ ([%d:]+) (.+)$")
if not (time and text) then
DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
return
end
local min, sec = string.split(":", time)
min = tonumber(min or "") or 0
sec = tonumber(sec or "")
if min and not sec then
sec = min
min = 0
end
time = min * 60 + sec
DBM:CreatePizzaTimer(time, text)
elseif cmd:sub(1, 15) == "broadcast timer" then
local time, text = msg:match("^%w+ %w+ ([%d:]+) (.+)$")
if DBM:GetRaidRank() == 0 then
DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
end
if not (time and text) then
DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
return
end
local min, sec = string.split(":", time)
min = tonumber(min or "") or 0
sec = tonumber(sec or "")
if min and not sec then
sec = min
min = 0
end
time = min * 60 + sec
DBM:CreatePizzaTimer(time, text, true)
elseif cmd:sub(0,5) == "break" then
if DBM:GetRaidRank() == 0 then
DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
return
end
local timer = tonumber(cmd:sub(6)) or 5
local timer = timer * 60
local channel = ((GetNumRaidMembers() == 0) and "PARTY") or "RAID_WARNING"
DBM:CreatePizzaTimer(timer, DBM_CORE_TIMER_BREAK, true)
DBM:Unschedule(SendChatMessage)
SendChatMessage(DBM_CORE_BREAK_START:format(timer/60), channel)
if timer/60 > 5 then DBM:Schedule(timer - 5*60, SendChatMessage, DBM_CORE_BREAK_MIN:format(5), channel) end
if timer/60 > 2 then DBM:Schedule(timer - 2*60, SendChatMessage, DBM_CORE_BREAK_MIN:format(2), channel) end
if timer/60 > 1 then DBM:Schedule(timer - 1*60, SendChatMessage, DBM_CORE_BREAK_MIN:format(1), channel) end
if timer > 30 then DBM:Schedule(timer - 30, SendChatMessage, DBM_CORE_BREAK_SEC:format(30), channel) end
DBM:Schedule(timer, SendChatMessage, DBM_CORE_ANNOUNCE_BREAK_OVER, channel)
elseif cmd:sub(1, 4) == "pull" then
if DBM:GetRaidRank() == 0 then
return DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
end
DBM:Unschedule(SendChatMessage)
fireEvent("DBM_TimerStop", "DBMPizzaTimer")
if DBM:GetRaidRank() >= 1 then
sendSync("DBMv4-Pizza-Cancel")
end
local timer = tonumber(cmd:sub(5)) or 10
local pullMessage = string.lower(cmd:sub(6) or "")
local channel = ((GetNumRaidMembers() == 0) and "PARTY") or "RAID_WARNING"
DBM:CreatePizzaTimer(timer, DBM_CORE_TIMER_PULL, true)
if timer > 1 then SendChatMessage(DBM_CORE_ANNOUNCE_PULL:format(timer), channel) end
if timer > 10 then DBM:Schedule(timer - 10, SendChatMessage, DBM_CORE_ANNOUNCE_PULL:format(10), channel) end
if timer > 7 then DBM:Schedule(timer - 7, SendChatMessage, DBM_CORE_ANNOUNCE_PULL:format(7), channel) end
if timer > 5 then DBM:Schedule(timer - 5, SendChatMessage, DBM_CORE_ANNOUNCE_PULL:format(5), channel) end
if timer > 3 then DBM:Schedule(timer - 3, SendChatMessage, DBM_CORE_ANNOUNCE_PULL:format(3), channel) end
if timer > 2 then DBM:Schedule(timer - 2, SendChatMessage, DBM_CORE_ANNOUNCE_PULL:format(2), channel) end
if timer > 1 then DBM:Schedule(timer - 1, SendChatMessage, DBM_CORE_ANNOUNCE_PULL:format(1), channel) end
if timer > 1 then DBM:Schedule(timer, SendChatMessage, DBM_CORE_ANNOUNCE_PULL_NOW, channel) end
if timer < 1 or pullMessage == "cancel" then
DBM:Unschedule(SendChatMessage)
fireEvent("DBM_TimerStop", "DBMPizzaTimer")
if DBM:GetRaidRank() >= 1 then
sendSync("DBMv4-Pizza-Cancel")
end
SendChatMessage(DBM_CORE_ANNOUNCE_PULL_CANCEL, channel)
end
if timer == 0 then end
elseif cmd:sub(1, 5) == "arrow" then
if not DBM:IsInRaid() then
DBM:AddMsg(DBM_ARROW_NO_RAIDGROUP)
return false
end
local x, y = string.split(" ", cmd:sub(6):trim())
xNum, yNum = tonumber(x or ""), tonumber(y or "")
local success
if xNum and yNum then
DBM.Arrow:ShowRunTo(xNum / 100, yNum / 100, 0)
success = true
elseif type(x) == "string" and x:trim() ~= "" then
local subCmd = x:trim()
if subCmd:upper() == "HIDE" then
DBM.Arrow:Hide()
success = true
elseif subCmd:upper() == "MOVE" then
DBM.Arrow:Move()
success = true
elseif subCmd:upper() == "TARGET" then
DBM.Arrow:ShowRunTo("target")
success = true
elseif subCmd:upper() == "FOCUS" then
DBM.Arrow:ShowRunTo("focus")
success = true
elseif DBM:GetRaidUnitId(DBM:Capitalize(subCmd)) ~= "none" then
DBM.Arrow:ShowRunTo(DBM:Capitalize(subCmd))
success = true
end
end
if not success then
for i, v in ipairs(DBM_ARROW_ERROR_USAGE) do
DBM:AddMsg(v)
end
end
else
DBM:LoadGUI()
end
end
SlashCmdList["PULL"] = function(msg) SlashCmdList["DEADLYBOSSMODS"]("pull "..msg) end
SLASH_DBMRANGE1 = "/range"
SLASH_DBMRANGE2 = "/distance"
SlashCmdList["DBMRANGE"] = function(msg)
if DBM.RangeCheck:IsShown() then
DBM.RangeCheck:Hide()
else
local r = tonumber(msg)
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
DBM.RangeCheck:Show(r)
else
DBM.RangeCheck:Show(10)
end
end
end
do
local sortMe = {}
local function sort(v1, v2)
return (v1.revision or 0) > (v2.revision or 0)
end
function DBM:ShowVersions(notify)
for i, v in pairs(raid) do
table.insert(sortMe, v)
end
table.sort(sortMe, sort)
self:AddMsg(DBM_CORE_VERSIONCHECK_HEADER)
for i, v in ipairs(sortMe) do
if v.displayVersion then
self:AddMsg(DBM_CORE_VERSIONCHECK_ENTRY:format(v.name, v.displayVersion, v.revision))
if notify and v.displayVersion ~= DBM.Version and v.revision < DBM.ReleaseRevision then
SendChatMessage(chatPrefixShort..DBM_CORE_YOUR_VERSION_OUTDATED, "WHISPER", nil, v.name)
end
else
self:AddMsg(DBM_CORE_VERSIONCHECK_ENTRY_NO_DBM:format(v.name))
end
end
for i = #sortMe, 1, -1 do
if not sortMe[i].revision then
table.remove(sortMe, i)
end
end
self:AddMsg(DBM_CORE_VERSIONCHECK_FOOTER:format(#sortMe))
for i = #sortMe, 1, -1 do
sortMe[i] = nil
end
end
--[[ hmm don't think that this is realy good, so disabled for the moment
function DBM:ElectMaster()
-- FIXME: Add Zonecheck for raidmates
local elect_player = nil
local elect_revision = tonumber(DBM.Revision)
local electd_raidlead = false
-- first of all, we only import the ranked mates
for i, v in pairs(raid) do
if v.rank >= 1 then
table.insert(sortMe, v)
end
end
table.sort(sortMe, sort)
if not #sortMe then return nil end -- no raid, no election
local p = sortMe[1]
if p.revision >= tonumber(DBM.Revision) then -- first we check the latest revision
DBM:AddMsg("Newest Version seems to be Revision of "..p.name.." r"..p.revision.." - local revision = r"..DBM.Revision)
elect_revision = tonumber(p.revision)
end
for i, v in ipairs(sortMe) do -- now we kick all assists with a revision lower than the hightest
if tonumber(v.revision) < elect_revision then
table.remove(sortMe, i)
end
end
for i, v in ipairs(sortMe) do -- we prefere to elect the Raidleader so we try this
if v.rank >= 2 then
DBM:AddMsg("Revision of "..v.name.." is "..v.revision.." and thats the RaidLeader")
elect_player = v.name
elect_revision = tonumber(v.revision)
elect_raidlead = true
end
end
if not elect_raidlead then
table.sort(sortMe, function(v1, v2) return v1.name > v2.name end) -- order by Name
if sortMe[#sortMe] then
p = sortMe[#sortMe]
DBM:AddMsg("Elected "..p.name.." is assist and best name")
elect_player = p.name
elect_revision = tonumber(p.revision)
end
end
table.wipe(sortMe)
return elect_player, elect_revision, elect_raidlead
end
--]]
end
-------------------
-- Pizza Timer --
-------------------
do
local ignore = {}
function DBM:CreatePizzaTimer(time, text, broadcast, sender)
if sender and ignore[sender] then return end
text = text:sub(1, 16)
text = text:gsub("%%t", UnitName("target") or "<no target>")
if text == DBM_CORE_TIMER_PULL then
self.Bars:CreateBar(time, text, "Interface\\Icons\\Ability_Warrior_Charge")
fireEvent("DBM_TimerStart", "DBMPizzaTimer", text, time, "Interface\\Icons\\Ability_Warrior_Charge", 0)
else
self.Bars:CreateBar(time, text)
fireEvent("DBM_TimerStart", "DBMPizzaTimer", text, time, "Interface\\Icons\\_DeathCoilV2_Red", 0)
end
if broadcast and self:GetRaidRank() >= 1 then
sendSync("DBMv4-Pizza", ("%s\t%s"):format(time, text))
end
if sender then DBM:ShowPizzaInfo(text, sender) end
end
function DBM:AddToPizzaIgnore(name)
ignore[name] = true
end
end
function DBM:ShowPizzaInfo(id, sender)
self:AddMsg(DBM_PIZZA_SYNC_INFO:format(sender, id))
end
------------------
-- Hyperlinks --
------------------
do
local ignore, cancel
StaticPopupDialogs["DBM_CONFIRM_IGNORE"] = {
text = DBM_PIZZA_CONFIRM_IGNORE,
button1 = YES,
button2 = NO,
OnAccept = function(self)
DBM:AddToPizzaIgnore(ignore)
DBM.Bars:CancelBar(cancel)
end,
timeout = 0,
hideOnEscape = 1,
}
DEFAULT_CHAT_FRAME:HookScript("OnHyperlinkClick", function(self, link, string, button, ...)
local linkType, arg1, arg2, arg3 = strsplit(":", link)
if linkType == "DBM" and arg1 == "cancel" then
DBM.Bars:CancelBar(link:match("DBM:cancel:(.+):nil$"))
elseif linkType == "DBM" and arg1 == "ignore" then
cancel = link:match("DBM:ignore:(.+):[^%s:]+$")
ignore = link:match(":([^:]+)$")
StaticPopup_Show("DBM_CONFIRM_IGNORE", ignore)
elseif linkType == "DBM" and arg1 == "update" then
DBM:ShowUpdateReminder(arg2, arg3) -- displayVersion, revision
end
end)
end
do
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
function ItemRefTooltip:SetHyperlink(link, ...)
if link:match("^DBM") then return end
return old(self, link, ...)
end
end
-----------------
-- GUI Stuff --
-----------------
do
local callOnLoad = {}
function DBM:LoadGUI()
if not IsAddOnLoaded("DBM-GUI") then
local _, _, _, enabled = GetAddOnInfo("DBM-GUI")
if not enabled then
EnableAddOn("DBM-GUI")
end
local loaded, reason = LoadAddOn("DBM-GUI")
if not loaded then
if reason then
self:AddMsg(DBM_CORE_LOAD_GUI_ERROR:format(tostring(_G["ADDON_"..reason or ""])))
else
self:AddMsg(DBM_CORE_LOAD_GUI_ERROR:format(DBM_CORE_UNKNOWN))
end
return false
end
table.sort(callOnLoad, function(v1, v2) return v1[2] < v2[2] end)
for i, v in ipairs(callOnLoad) do v[1]() end
collectgarbage("collect")
end
return DBM_GUI:ShowHide()
end
function DBM:RegisterOnGuiLoadCallback(f, sort)
table.insert(callOnLoad, {f, sort or math.huge})
end
end
----------------------
-- Minimap Button --
----------------------
do
local dragMode = nil
local function moveButton(self)
if dragMode == "free" then
local centerX, centerY = Minimap:GetCenter()
local x, y = GetCursorPosition()
x, y = x / self:GetEffectiveScale() - centerX, y / self:GetEffectiveScale() - centerY
self:ClearAllPoints()
self:SetPoint("CENTER", x, y)
else
local centerX, centerY = Minimap:GetCenter()
local x, y = GetCursorPosition()
x, y = x / self:GetEffectiveScale() - centerX, y / self:GetEffectiveScale() - centerY
centerX, centerY = math.abs(x), math.abs(y)
centerX, centerY = (centerX / math.sqrt(centerX^2 + centerY^2)) * 80, (centerY / sqrt(centerX^2 + centerY^2)) * 80
centerX = x < 0 and -centerX or centerX
centerY = y < 0 and -centerY or centerY
self:ClearAllPoints()
self:SetPoint("CENTER", centerX, centerY)
end
end
local button = CreateFrame("Button", "DBMMinimapButton", Minimap)
button:SetHeight(32)
button:SetWidth(32)
button:SetFrameStrata("MEDIUM")
button:SetPoint("CENTER", -65.35, -38.8)
button:SetMovable(true)
button:SetUserPlaced(true)
button:SetNormalTexture("Interface\\AddOns\\DBM-Core\\textures\\Minimap-Button-Up")
button:SetPushedTexture("Interface\\AddOns\\DBM-Core\\textures\\Minimap-Button-Down")
button:SetHighlightTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight")
button:SetScript("OnMouseDown", function(self, button)
if IsShiftKeyDown() and IsAltKeyDown() then
dragMode = "free"
self:SetScript("OnUpdate", moveButton)
elseif IsShiftKeyDown() or button == "RightButton" then
dragMode = nil
self:SetScript("OnUpdate", moveButton)
end
end)
button:SetScript("OnMouseUp", function(self)
self:SetScript("OnUpdate", nil)
end)
button:SetScript("OnClick", function(self, button)
if IsShiftKeyDown() or button == "RightButton" then return end
DBM:LoadGUI()
end)
button:SetScript("OnEnter", function(self)
GameTooltip_SetDefaultAnchor(GameTooltip, self)
GameTooltip:SetText(DBM_CORE_MINIMAP_TOOLTIP_HEADER, 1, 1, 1)
GameTooltip:AddLine(ver, NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b, 1)
GameTooltip:AddLine(" ")
GameTooltip:AddLine(DBM_CORE_MINIMAP_TOOLTIP_FOOTER, RAID_CLASS_COLORS.MAGE.r, RAID_CLASS_COLORS.MAGE.g, RAID_CLASS_COLORS.MAGE.b, 1)
GameTooltip:Show()
end)
button:SetScript("OnLeave", function(self)
GameTooltip:Hide()
end)
function DBM:ToggleMinimapButton()
self.Options.ShowMinimapButton = not self.Options.ShowMinimapButton
if self.Options.ShowMinimapButton then
button:Show()
else
button:Hide()
end
end
function DBM:HideMinimapButton()
return button:Hide()
end
end
---------------------------
-- Raid/Party Handling --
---------------------------
do
local inRaid = false
local playerRank = 0
function DBM:RAID_ROSTER_UPDATE()
if GetNumRaidMembers() >= 1 then
local playerWithHigherVersionPromoted = false
for i = 1, GetNumRaidMembers() do
local name, rank, subgroup, _, _, fileName = GetRaidRosterInfo(i)
if (not raid[name]) and inRaid then
fireEvent("raidJoin", name)
end
raid[name] = raid[name] or {}
raid[name].name = name
raid[name].rank = rank
raid[name].subgroup = subgroup
raid[name].class = fileName
raid[name].id = "raid"..i
raid[name].updated = true
if not playerWithHigherVersionPromoted and rank >= 1 and raid[name].version and raid[name].version > tonumber(DBM.Version) then
playerWithHigherVersionPromoted = true
end
end
enableIcons = not playerWithHigherVersionPromoted
if not inRaid then
inRaid = true
sendSync("DBMv4-Ver", "Hi!")
self:Schedule(2, DBM.RequestTimers, DBM)
fireEvent("raidJoin", UnitName("player"))
end
for i, v in pairs(raid) do
if not v.updated then
raid[i] = nil
fireEvent("raidLeave", i)
else
v.updated = nil
end
end
else
inRaid = false
enableIcons = true
fireEvent("raidLeave", UnitName("player"))
end
end
function DBM:PARTY_MEMBERS_CHANGED()
if GetNumRaidMembers() > 0 then return end
if GetNumPartyMembers() >= 1 then
if not inRaid then
inRaid = true
sendSync("DBMv4-Ver", "Hi!")
self:Schedule(2, DBM.RequestTimers, DBM)
fireEvent("partyJoin", UnitName("player"))
end
for i = 0, GetNumPartyMembers() do
local id
if (i == 0) then
id = "player"
else
id = "party"..i
end
local name, server = UnitName(id)
local rank, _, fileName = UnitIsPartyLeader(id), UnitClass(id)
if server and server ~= "" then
name = name.."-"..server
end
if (not raid[name]) and inRaid then
fireEvent("partyJoin", name)
end
raid[name] = raid[name] or {}
raid[name].name = name
if rank then
raid[name].rank = 2
else
raid[name].rank = 0
end
raid[name].class = fileName
raid[name].id = id
raid[name].updated = true
end
for i, v in pairs(raid) do
if not v.updated then
raid[i] = nil
fireEvent("partyLeave", i)
else
v.updated = nil
end
end
else
inRaid = false
enableIcons = true
end
end
function DBM:IsInRaid()
return inRaid
end
function DBM:GetRaidRank(name)
name = name or UnitName("player")
return (raid[name] and raid[name].rank) or 0
end
function DBM:GetRaidSubgroup(name)
name = name or UnitName("player")
return (raid[name] and raid[name].subgroup) or 0
end
function DBM:GetRaidClass(name)
name = name or UnitName("player")
return (raid[name] and raid[name].class) or "UNKNOWN"
end
function DBM:GetRaidUnitId(name)
name = name or UnitName("player")
return (raid[name] and raid[name].id) or "none"
end
end
---------------
-- Options --
---------------
do
local function addDefaultOptions(t1, t2)
for i, v in pairs(t2) do
if t1[i] == nil then
t1[i] = v
elseif type(v) == "table" then
addDefaultOptions(v, t2[i])
end
end
end
local function setRaidWarningPositon()
RaidWarningFrame:ClearAllPoints()
RaidWarningFrame:SetPoint(DBM.Options.RaidWarningPosition.Point, UIParent, DBM.Options.RaidWarningPosition.Point, DBM.Options.RaidWarningPosition.X, DBM.Options.RaidWarningPosition.Y)
end
function loadOptions()
DBM.Options = DBM_SavedOptions
addDefaultOptions(DBM.Options, DBM.DefaultOptions)
-- load special warning options
DBM:UpdateSpecialWarningOptions()
-- set this with a short delay to prevent issues with other addons also trying to do the same thing with another position ;)
DBM:Schedule(5, setRaidWarningPositon)
end
local defaultStats = {
kills = 0,
pulls = 0,
heroic10Kills = 0,
heroic25Kills = 0,
heroic10Pulls = 0,
heroic25Pulls = 0,
};
function loadModOptions(modId)
local savedOptions = _G[modId:gsub("-", "").."_SavedVars"] or {}
local savedStats = _G[modId:gsub("-", "").."_SavedStats"] or {}
for i, v in ipairs(DBM.Mods) do
if v.modId == modId then
savedOptions[v.id] = savedOptions[v.id] or v.Options
for option, optionValue in pairs(v.Options) do
if savedOptions[v.id][option] == nil then
savedOptions[v.id][option] = optionValue
end
end
v.Options = savedOptions[v.id] or {}
savedStats[v.id] = savedStats[v.id] or {};
for x,y in pairs(defaultStats) do
savedStats[v.id][x] = savedStats[v.id][x] or y;
end
v.stats = savedStats[v.id]
if v.OnInitialize then v:OnInitialize() end
for i, cat in ipairs(v.categorySort) do -- temporary hack
if cat == "misc" then
table.remove(v.categorySort, i)
table.insert(v.categorySort, cat)
break
end
end
end
end
_G[modId:gsub("-", "").."_SavedVars"] = savedOptions
_G[modId:gsub("-", "").."_SavedStats"] = savedStats
end
end
--------------
-- OnLoad --
--------------
do
local function showOldVerWarning()
StaticPopupDialogs["DBM_OLD_VERSION"] = {
text = DBM_CORE_ERROR_DBMV3_LOADED,
button1 = DBM_CORE_OK,
OnAccept = function()
DisableAddOn("DBM_API")
ReloadUI()
end,
timeout = 0,
exclusive = 1,
whileDead = 1,
}
StaticPopup_Show("DBM_OLD_VERSION")
end
local function setCombatInitialized()
combatInitialized = true
end
function DBM:ADDON_LOADED(modname)
if modname == "DBM-Core" then
loadOptions()
DBM.Bars:LoadOptions("DBM");
--for id,barGroup in pairs(DBM.BarGroups) do
-- barGroup:LoadOptions(id)
--end
DBM.Arrow:LoadPosition()
if not DBM.Options.ShowMinimapButton then DBM:HideMinimapButton() end
self.AddOns = {}
for i = 1, GetNumAddOns() do
if GetAddOnMetadata(i, "X-DBM-Mod") and not checkEntry(bannedMods, GetAddOnInfo(i)) then
table.insert(self.AddOns, {
sort = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Sort") or math.huge) or math.huge,
category = GetAddOnMetadata(i, "X-DBM-Mod-Category") or "Other",
name = GetAddOnMetadata(i, "X-DBM-Mod-Name") or "",
zone = {strsplit(",", GetAddOnMetadata(i, "X-DBM-Mod-LoadZone") or "")},
zoneId = {strsplit(",", GetAddOnMetadata(i, "X-DBM-Mod-LoadZoneID") or "")},
subTabs = GetAddOnMetadata(i, "X-DBM-Mod-SubCategories") and {strsplit(",", GetAddOnMetadata(i, "X-DBM-Mod-SubCategories"))},
hasHeroic = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Has-Heroic-Mode") or 1) == 1,
modId = GetAddOnInfo(i),
})
for k, v in ipairs(self.AddOns[#self.AddOns].zone) do
self.AddOns[#self.AddOns].zone[k] = (self.AddOns[#self.AddOns].zone[k]):trim()
end
for i = #self.AddOns[#self.AddOns].zoneId, 1, -1 do
local id = tonumber(self.AddOns[#self.AddOns].zoneId[i])
if id then
self.AddOns[#self.AddOns].zoneId[i] = id
else
table.remove(self.AddOns[#self.AddOns].zoneId, i)
end
end
if self.AddOns[#self.AddOns].subTabs then
for k, v in ipairs(self.AddOns[#self.AddOns].subTabs) do
self.AddOns[#self.AddOns].subTabs[k] = (self.AddOns[#self.AddOns].subTabs[k]):trim()
end
end
end
end
table.sort(self.AddOns, function(v1, v2) return v1.sort < v2.sort end)
self:RegisterEvents(
"COMBAT_LOG_EVENT_UNFILTERED",
"ZONE_CHANGED_NEW_AREA",
"RAID_ROSTER_UPDATE",
"PARTY_MEMBERS_CHANGED",
"CHAT_MSG_ADDON",
"PLAYER_REGEN_DISABLED",
"UNIT_DIED",
"UNIT_DESTROYED",
"CHAT_MSG_WHISPER",
"CHAT_MSG_BN_WHISPER",
"CHAT_MSG_MONSTER_YELL",
"CHAT_MSG_MONSTER_EMOTE",
"CHAT_MSG_MONSTER_SAY",
"CHAT_MSG_RAID_BOSS_EMOTE",
"PLAYER_ENTERING_WORLD",
"LFG_PROPOSAL_SHOW",
"LFG_PROPOSAL_FAILED",
"LFG_UPDATE"
)
self:ZONE_CHANGED_NEW_AREA()
self:RAID_ROSTER_UPDATE()
self:PARTY_MEMBERS_CHANGED()
DBM:Schedule(1.5, setCombatInitialized)
local enabled, loadable = select(4, GetAddOnInfo("DBM_API"))
if enabled and loadable then showOldVerWarning() end
end
end
end
function DBM:LFG_PROPOSAL_SHOW()
DBM.Bars:CreateBar(40, DBM_LFG_INVITE, "Interface\\Icons\\Spell_Holy_BorrowedTime")
fireEvent("DBM_TimerStart", "DBMLFGTimer", "Dungeon Finder", 40, "237538", "extratimer", nil, 0)
end
function DBM:LFG_PROPOSAL_FAILED()
DBM.Bars:CancelBar(DBM_LFG_INVITE)
fireEvent("DBM_TimerStop", "DBMLFGTimer")
end
function DBM:LFG_UPDATE()
local _, joined = GetLFGInfoServer()
if not joined then
DBM.Bars:CancelBar(DBM_LFG_INVITE)
end
end
--------------------------------
-- Load Boss Mods on Demand --
--------------------------------
function DBM:ZONE_CHANGED_NEW_AREA()
local zoneName = GetRealZoneText()
local zoneId = GetCurrentMapAreaID()
for i, v in ipairs(self.AddOns) do
if not IsAddOnLoaded(v.modId) and (checkEntry(v.zone, zoneName) or checkEntry(v.zoneId, zoneId)) then
-- srsly, wtf? LoadAddOn doesn't work properly on ZONE_CHANGED_NEW_AREA when reloading the UI
-- TODO: is this still necessary? this was a WotLK beta bug
DBM:Unschedule(DBM.LoadMod, DBM, v)
DBM:Schedule(3, DBM.LoadMod, DBM, v)
end
end
if select(2, IsInInstance()) == "pvp" and not DBM:GetModByName("AlteracValley") then
for i, v in ipairs(DBM.AddOns) do
if v.modId == "DBM-PvP" then
DBM:LoadMod(v)
break
end
end
end
end
function DBM:LoadMod(mod)
if type(mod) ~= "table" then return false end
local _, _, _, enabled = GetAddOnInfo(mod.modId)
if not enabled then
EnableAddOn(mod.modId)
end
local loaded, reason = LoadAddOn(mod.modId)
if not loaded then
if reason then
self:AddMsg(DBM_CORE_LOAD_MOD_ERROR:format(tostring(mod.name), tostring(_G["ADDON_"..reason or ""])))
else
-- 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...)
end
return false
else
self:AddMsg(DBM_CORE_LOAD_MOD_SUCCESS:format(tostring(mod.name)))
loadModOptions(mod.modId)
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
if v.modId == mod.modId then
v.hasHeroic = mod.hasHeroic
end
end
if DBM_GUI then
DBM_GUI:UpdateModList()
end
collectgarbage("collect")
return true
end
end
do
if select(4, GetAddOnInfo("DBM-PvP")) and select(5, GetAddOnInfo("DBM-PvP")) then
local checkBG
function checkBG()
if not DBM:GetModByName("AlteracValley") and MAX_BATTLEFIELD_QUEUES then
for i = 1, MAX_BATTLEFIELD_QUEUES do
if GetBattlefieldStatus(i) == "confirm" then
for i, v in ipairs(DBM.AddOns) do
if v.modId == "DBM-PvP" then
DBM:LoadMod(v)
return
end
end
end
end
DBM:Schedule(1, checkBG)
end
end
DBM:Schedule(1, checkBG)
end
end
-----------------------------
-- Handle Incoming Syncs --
-----------------------------
do
local syncHandlers = {}
local whisperSyncHandlers = {}
syncHandlers["DBMv4-Mod"] = function(msg, channel, sender)
local mod, revision, event, arg = strsplit("\t", msg)
mod = DBM:GetModByName(mod or "")
if mod and event and arg and revision then
revision = tonumber(revision) or 0
mod:ReceiveSync(event, arg, sender, revision)
end
end
syncHandlers["DBMv4-Pull"] = function(msg, channel, sender)
if select(2, IsInInstance()) == "pvp" then return end
local delay, mod, revision = strsplit("\t", msg)
local lag = select(3, GetNetStats()) / 1000
delay = tonumber(delay or 0) or 0
revision = tonumber(revision or 0) or 0
mod = DBM:GetModByName(mod or "")
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
DBM:StartCombat(mod, delay + lag, true)
end
end
syncHandlers["DBMv4-Kill"] = function(msg, channel, sender)
if select(2, IsInInstance()) == "pvp" then return end
local cId = tonumber(msg)
if cId then DBM:OnMobKill(cId, true) end
end
syncHandlers["DBMv4-Ver"] = function(msg, channel, sender)
if msg == "Hi!" then
sendSync("DBMv4-Ver", ("%s\t%s\t%s\t%s"):format(DBM.Revision, DBM.Version, DBM.DisplayVersion, GetLocale()))
else
local revision, version, displayVersion, locale = strsplit("\t", msg)
revision, version = tonumber(revision or ""), tonumber(version or "")
if revision and version and displayVersion and raid[sender] then
raid[sender].revision = revision
raid[sender].version = version
raid[sender].displayVersion = displayVersion
raid[sender].locale = locale
if version > tonumber(DBM.Version) then
if raid[sender].rank >= 1 then
enableIcons = false
end
if not showedUpdateReminder then
local found = false
for i, v in pairs(raid) do
if v.version == version and v ~= raid[sender] then
found = true
break
end
end
if found then
showedUpdateReminder = true
if not DBM.Options.BlockVersionUpdatePopup then
DBM:ShowUpdateReminder(displayVersion, revision)
else
DBM:AddMsg(DBM_CORE_UPDATEREMINDER_HEADER:match("([^\n]*)"))
DBM:AddMsg(DBM_CORE_UPDATEREMINDER_HEADER:match("\n(.*)"):format(displayVersion, revision))
DBM:AddMsg(("|HDBM:update:%s:%s|h|cff3588ff[https://discord.gg/4ZHfgskSvM]"):format(displayVersion, revision))
end
end
end
end
end
end
end
syncHandlers["DBMv4-Pizza"] = function(msg, channel, sender)
if select(2, IsInInstance()) == "pvp" then return end
if DBM:GetRaidRank(sender) == 0 then return end
if sender == UnitName("player") then return end
local time, text = strsplit("\t", msg)
time = tonumber(time or 0)
text = tostring(text)
if time and text then
DBM:CreatePizzaTimer(time, text, nil, sender)
end
end
syncHandlers["DBMv4-Pizza-Cancel"] = function(msg, channel, sender)
if select(2, IsInInstance()) == "pvp" then return end
if DBM:GetRaidRank(sender) == 0 then return end
if sender == UnitName("player") then return end
DBM:Unschedule(SendChatMessage)
fireEvent("DBM_TimerStop", "DBMPizzaTimer")
end
whisperSyncHandlers["DBMv4-RequestTimers"] = function(msg, channel, sender)
DBM:SendTimers(sender)
end
whisperSyncHandlers["DBMv4-CombatInfo"] = function(msg, channel, sender)
local mod, time = strsplit("\t", msg)
mod = DBM:GetModByName(mod or "")
time = tonumber(time or 0)
if mod and time then
DBM:ReceiveCombatInfo(sender, mod, time)
end
end
whisperSyncHandlers["DBMv4-TimerInfo"] = function(msg, channel, sender)
local mod, timeLeft, totalTime, id = strsplit("\t", msg)
mod = DBM:GetModByName(mod or "")
timeLeft = tonumber(timeLeft or 0)
totalTime = tonumber(totalTime or 0)
if mod and timeLeft and timeLeft > 0 and totalTime and totalTime > 0 and id then
DBM:ReceiveTimerInfo(sender, mod, timeLeft, totalTime, id, select(5, strsplit("\t", msg)))
end
end
function DBM:CHAT_MSG_ADDON(prefix, msg, channel, sender)
if msg and channel ~= "WHISPER" and channel ~= "GUILD" then
local handler = syncHandlers[prefix]
if handler then handler(msg, channel, sender) end
elseif msg and channel == "WHISPER" and self:GetRaidUnitId(sender) ~= "none" then
local handler = whisperSyncHandlers[prefix]
if handler then handler(msg, channel, sender) end
end
end
end
-----------------------
-- Update Reminder --
-----------------------
function DBM:ShowUpdateReminder(newVersion, newRevision)
local frame = CreateFrame("Frame", nil, UIParent)
frame:SetFrameStrata("DIALOG")
frame:SetWidth(430)
frame:SetHeight(155)
frame:SetPoint("TOP", 0, -230)
frame:SetBackdrop({
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", tile = true, tileSize = 32, edgeSize = 32,
insets = {left = 11, right = 12, top = 12, bottom = 11},
})
local fontstring = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
fontstring:SetWidth(410)
fontstring:SetHeight(0)
fontstring:SetPoint("TOP", 0, -16)
fontstring:SetText(DBM_CORE_UPDATEREMINDER_HEADER:format(newVersion, newRevision))
local editBox = CreateFrame("EditBox", nil, frame)
do
local editBoxLeft = editBox:CreateTexture(nil, "BACKGROUND")
local editBoxRight = editBox:CreateTexture(nil, "BACKGROUND")
local editBoxMiddle = editBox:CreateTexture(nil, "BACKGROUND")
editBoxLeft:SetTexture("Interface\\ChatFrame\\UI-ChatInputBorder-Left")
editBoxLeft:SetHeight(32)
editBoxLeft:SetWidth(32)
editBoxLeft:SetPoint("LEFT", -14, 0)
editBoxLeft:SetTexCoord(0, 0.125, 0, 1)
editBoxRight:SetTexture("Interface\\ChatFrame\\UI-ChatInputBorder-Right")
editBoxRight:SetHeight(32)
editBoxRight:SetWidth(32)
editBoxRight:SetPoint("RIGHT", 6, 0)
editBoxRight:SetTexCoord(0.875, 1, 0, 1)
editBoxMiddle:SetTexture("Interface\\ChatFrame\\UI-ChatInputBorder-Right")
editBoxMiddle:SetHeight(32)
editBoxMiddle:SetWidth(1)
editBoxMiddle:SetPoint("LEFT", editBoxLeft, "RIGHT")
editBoxMiddle:SetPoint("RIGHT", editBoxRight, "LEFT")
editBoxMiddle:SetTexCoord(0, 0.9375, 0, 1)
end
editBox:SetHeight(32)
editBox:SetWidth(250)
editBox:SetPoint("TOP", fontstring, "BOTTOM", 0, -4)
editBox:SetFontObject("GameFontHighlight")
editBox:SetTextInsets(0, 0, 0, 1)
editBox:SetFocus()
editBox:SetText("https://discord.gg/4ZHfgskSvM")
editBox:HighlightText()
editBox:SetScript("OnTextChanged", function(self)
editBox:SetText("https://discord.gg/4ZHfgskSvM")
editBox:HighlightText()
end)
local fontstring = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
fontstring:SetWidth(410)
fontstring:SetHeight(0)
fontstring:SetPoint("TOP", editBox, "BOTTOM", 0, 0)
fontstring:SetText(DBM_CORE_UPDATEREMINDER_FOOTER)
local button = CreateFrame("Button", nil, frame)
button:SetHeight(24)
button:SetWidth(75)
button:SetPoint("BOTTOM", 0, 13)
button:SetNormalFontObject("GameFontNormal")
button:SetHighlightFontObject("GameFontHighlight")
button:SetNormalTexture(button:CreateTexture(nil, nil, "UIPanelButtonUpTexture"))
button:SetPushedTexture(button:CreateTexture(nil, nil, "UIPanelButtonDownTexture"))
button:SetHighlightTexture(button:CreateTexture(nil, nil, "UIPanelButtonHighlightTexture"))
button:SetText(DBM_CORE_OK)
button:SetScript("OnClick", function(self)
frame:Hide()
end)
end
----------------------
-- Pull Detection --
----------------------
do
local targetList = {}
local function buildTargetList()
local uId = ((GetNumRaidMembers() == 0) and "party") or "raid"
for i = 0, math.max(GetNumRaidMembers(), GetNumPartyMembers()) do
local id = (i == 0 and "target") or uId..i.."target"
local guid = UnitGUID(id)
if guid and (bit.band(guid:sub(1, 5), 0x00F) == 3 or bit.band(guid:sub(1, 5), 0x00F) == 5) then
local cId = tonumber(guid:sub(9, 12), 16)
targetList[cId] = id
end
end
end
local function clearTargetList()
table.wipe(targetList)
end
local function scanForCombat(mod, mob)
if not checkEntry(inCombat, mod) then
buildTargetList()
if targetList[mob] and UnitAffectingCombat(targetList[mob]) then
DBM:StartCombat(mod, 3)
end
clearTargetList()
end
end
local function checkForPull(mob, combatInfo)
local uId = targetList[mob]
if uId and UnitAffectingCombat(uId) then
DBM:StartCombat(combatInfo.mod, 0)
return true
elseif uId then
DBM:Schedule(3, scanForCombat, combatInfo.mod, mob)
end
end
function DBM:PLAYER_REGEN_DISABLED()
if not combatInitialized then return end
if combatInfo[GetRealZoneText()] or combatInfo[GetCurrentMapAreaID()] then
buildTargetList()
if combatInfo[GetRealZoneText()] then
for i, v in ipairs(combatInfo[GetRealZoneText()]) do
if v.type == "combat" then
if v.multiMobPullDetection then
for _, mob in ipairs(v.multiMobPullDetection) do
if checkForPull(mob, v) then
break
end
end
else
checkForPull(v.mob, v)
end
end
end
end
-- copy & paste, lol
if combatInfo[GetCurrentMapAreaID()] then
for i, v in ipairs(combatInfo[GetCurrentMapAreaID()]) do
if v.type == "combat" then
if v.multiMobPullDetection then
for _, mob in ipairs(v.multiMobPullDetection) do
if checkForPull(mob, v) then
break
end
end
else
checkForPull(v.mob, v)
end
end
end
end
clearTargetList()
end
end
end
do
-- called for all mob chat events
local function onMonsterMessage(type, msg)
-- pull detection
if combatInfo[GetRealZoneText()] then
for i, v in ipairs(combatInfo[GetRealZoneText()]) do
if v.type == type and checkEntry(v.msgs, msg) then
DBM:StartCombat(v.mod, 0)
end
end
end
-- copy & paste, lol
if combatInfo[GetCurrentMapAreaID()] then
for i, v in ipairs(combatInfo[GetCurrentMapAreaID()]) do
if v.type == type and checkEntry(v.msgs, msg) then
DBM:StartCombat(v.mod, 0)
end
end
end
-- kill detection (wipe detection would also be nice to have)
-- todo: add sync
for i = #inCombat, 1, -1 do
local v = inCombat[i]
if not v.combatInfo then return end
if v.combatInfo.killType == type and v.combatInfo.killMsgs[msg] then
DBM:EndCombat(v)
end
end
end
function DBM:CHAT_MSG_MONSTER_YELL(msg)
return onMonsterMessage("yell", msg)
end
function DBM:CHAT_MSG_MONSTER_EMOTE(msg)
return onMonsterMessage("emote", msg)
end
function DBM:CHAT_MSG_RAID_BOSS_EMOTE(msg, ...)
onMonsterMessage("emote", msg)
return self:FilterRaidBossEmote(msg, ...)
end
function DBM:CHAT_MSG_MONSTER_SAY(msg)
return onMonsterMessage("say", msg)
end
end
---------------------------
-- Kill/Wipe Detection --
---------------------------
function checkWipe(confirm)
if #inCombat > 0 then
local wipe = true
local uId = ((GetNumRaidMembers() == 0) and "party") or "raid"
for i = 0, math.max(GetNumRaidMembers(), GetNumPartyMembers()) do
local id = (i == 0 and "player") or uId..i
if UnitAffectingCombat(id) and not UnitIsDeadOrGhost(id) then
wipe = false
break
end
end
if not wipe then
DBM:Schedule(3, checkWipe)
elseif confirm then
for i = #inCombat, 1, -1 do
DBM:EndCombat(inCombat[i], true)
end
else
local maxDelayTime = 5
for i, v in ipairs(inCombat) do
maxDelayTime = v.combatInfo and v.combatInfo.wipeTimer and v.combatInfo.wipeTimer > maxDelayTime and v.combatInfo.wipeTimer or maxDelayTime
end
DBM:Schedule(maxDelayTime, checkWipe, true)
end
end
end
function DBM:StartCombat(mod, delay, synced)
if not checkEntry(inCombat, mod) then
if not mod.combatInfo then return end
if mod.combatInfo.noCombatInVehicle and UnitInVehicle("player") then -- HACK
return
end
table.insert(inCombat, mod)
self:AddMsg(DBM_CORE_COMBAT_STARTED:format(mod.combatInfo.name))
if mod:IsDifficulty("heroic5", "heroic10") then
mod.stats.heroic10Pulls = mod.stats.heroic10Pulls + 1
elseif mod:IsDifficulty("heroic25") then
mod.stats.heroic25Pulls = mod.stats.heroic25Pulls + 1
elseif mod:IsDifficulty("normal5", "normal10", "normal25") then
mod.stats.pulls = mod.stats.pulls + 1
end
mod.inCombat = true
mod.blockSyncs = nil
mod.combatInfo.pull = GetTime() - (delay or 0)
self:Schedule(mod.minCombatTime or 3, checkWipe)
if (DBM.Options.AlwaysShowHealthFrame or mod.Options.HealthFrame) and mod.Options.Enabled then
DBM.BossHealth:Show(mod.localization.general.name)
if mod.bossHealthInfo then
for i = 1, #mod.bossHealthInfo, 2 do
DBM.BossHealth:AddBoss(mod.bossHealthInfo[i], mod.bossHealthInfo[i + 1])
end
else
DBM.BossHealth:AddBoss(mod.combatInfo.mob, mod.localization.general.name)
end
end
if mod.OnCombatStart and mod.Options.Enabled then mod:OnCombatStart(delay or 0) end
if not synced then
sendSync("DBMv4-Pull", (delay or 0).."\t"..mod.id.."\t"..(mod.revision or 0))
end
fireEvent("pull", mod, delay, synced)
-- http://www.deadlybossmods.com/forum/viewtopic.php?t=1464
if DBM.Options.ShowBigBrotherOnCombatStart and BigBrother and type(BigBrother.ConsumableCheck) == "function" then
if DBM.Options.BigBrotherAnnounceToRaid then
BigBrother:ConsumableCheck("RAID")
else
BigBrother:ConsumableCheck("SELF")
end
end
if DBM.Options.FixCLEUOnCombatStart then
CombatLogClearEntries()
end
end
end
function DBM:EndCombat(mod, wipe)
if removeEntry(inCombat, mod) then
mod:Stop()
mod.inCombat = false
mod.blockSyncs = true
if mod.combatInfo.killMobs then
for i, v in pairs(mod.combatInfo.killMobs) do
mod.combatInfo.killMobs[i] = true
end
end
if wipe then
local thisTime = GetTime() - mod.combatInfo.pull
if thisTime < 30 then
if mod:IsDifficulty("heroic5", "heroic10") then
mod.stats.heroic10Pulls = mod.stats.heroic10Pulls - 1
elseif mod:IsDifficulty("heroic25") then
mod.stats.heroic25Pulls = mod.stats.heroic25Pulls - 1
elseif mod:IsDifficulty("normal5", "normal10", "normal25") then
mod.stats.pulls = mod.stats.pulls - 1
end
end
self:AddMsg(DBM_CORE_COMBAT_ENDED:format(mod.combatInfo.name, strFromTime(thisTime)))
local msg
for k, v in pairs(autoRespondSpam) do
msg = msg or chatPrefixShort..DBM_CORE_WHISPER_COMBAT_END_WIPE:format(UnitName("player"), (mod.combatInfo.name or ""))
sendWhisper(k, msg)
end
fireEvent("wipe", mod)
fireEvent("DBM_Wipe", mod)
else
local thisTime = GetTime() - mod.combatInfo.pull
local lastTime = (mod:IsDifficulty("heroic5", "heroic10") and mod.stats.heroic10LastTime) or (mod:IsDifficulty("heroic25") and mod.stats.heroic25LastTime) or mod:IsDifficulty("normal5", "normal10", "normal25") and mod.stats.lastTime
local bestTime = (mod:IsDifficulty("heroic5", "heroic10") and mod.stats.heroic10BestTime) or (mod:IsDifficulty("heroic25") and mod.stats.heroic25BestTime) or mod:IsDifficulty("normal5", "normal10", "normal25") and mod.stats.bestTime
if mod:IsDifficulty("heroic5", "heroic10") then
mod.stats.heroic10Kills = mod.stats.heroic10Kills + 1
mod.stats.heroic10LastTime = thisTime
mod.stats.heroic10BestTime = math.min(bestTime or math.huge, thisTime)
elseif mod:IsDifficulty("heroic25") then
mod.stats.heroic25Kills = mod.stats.heroic25Kills + 1
mod.stats.heroic25LastTime = thisTime
mod.stats.heroic25BestTime = math.min(bestTime or math.huge, thisTime)
elseif mod:IsDifficulty("normal5", "normal10", "normal25") then
mod.stats.kills = mod.stats.kills + 1
mod.stats.lastTime = thisTime
mod.stats.bestTime = math.min(bestTime or math.huge, thisTime)
end
if not lastTime then
self:AddMsg(DBM_CORE_BOSS_DOWN:format(mod.combatInfo.name, strFromTime(thisTime)))
elseif thisTime < (bestTime or math.huge) then
self:AddMsg(DBM_CORE_BOSS_DOWN_NEW_RECORD:format(mod.combatInfo.name, strFromTime(thisTime), strFromTime(bestTime)))
else
self:AddMsg(DBM_CORE_BOSS_DOWN_LONG:format(mod.combatInfo.name, strFromTime(thisTime), strFromTime(lastTime), strFromTime(bestTime)))
end
local msg
for k, v in pairs(autoRespondSpam) do
msg = msg or chatPrefixShort..DBM_CORE_WHISPER_COMBAT_END_KILL:format(UnitName("player"), (mod.combatInfo.name or ""))
sendWhisper(k, msg)
end
fireEvent("kill", mod)
fireEvent("DBM_Kill", mod)
end
table.wipe(autoRespondSpam)
if mod.OnCombatEnd then mod:OnCombatEnd(wipe) end
DBM.BossHealth:Hide()
DBM.Arrow:Hide(true)
end
end
function DBM:OnMobKill(cId, synced)
for i = #inCombat, 1, -1 do
local v = inCombat[i]
if not v.combatInfo then
return
end
if v.combatInfo.killMobs and v.combatInfo.killMobs[cId] then
if not synced then
sendSync("DBMv4-Kill", cId)
end
v.combatInfo.killMobs[cId] = false
local allMobsDown = true
for i, v in pairs(v.combatInfo.killMobs) do
if v then
allMobsDown = false
break
end
end
if allMobsDown then
self:EndCombat(v)
end
elseif cId == v.combatInfo.mob and not v.combatInfo.killMobs and not v.combatInfo.multiMobPullDetection then
if not synced then
sendSync("DBMv4-Kill", cId)
end
self:EndCombat(v)
end
end
end
function DBM:UNIT_DIED(args)
if bit.band(args.destGUID:sub(1, 5), 0x00F) == 3 or bit.band(args.destGUID:sub(1, 5), 0x00F) == 5 then
self:OnMobKill(tonumber(args.destGUID:sub(9, 12), 16))
end
end
DBM.UNIT_DESTROYED = DBM.UNIT_DIED
----------------------
-- Timer recovery --
----------------------
do
local requestedFrom = nil
local requestTime = 0
function DBM:RequestTimers()
local bestClient
for i, v in pairs(raid) do
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
bestClient = v
end
end
if not bestClient then return end
requestedFrom = bestClient.name
requestTime = GetTime()
SendAddonMessage("DBMv4-RequestTimers", "", "WHISPER", bestClient.name)
end
function DBM:ReceiveCombatInfo(sender, mod, time)
if sender == requestedFrom and (GetTime() - requestTime) < 5 and #inCombat == 0 then
local lag = select(3, GetNetStats()) / 1000
if not mod.combatInfo then return end
table.insert(inCombat, mod)
mod.inCombat = true
mod.blockSyncs = nil
mod.combatInfo.pull = GetTime() - time + lag
self:Schedule(3, checkWipe)
end
end
function DBM:ReceiveTimerInfo(sender, mod, timeLeft, totalTime, id, ...)
if sender == requestedFrom and (GetTime() - requestTime) < 5 then
local lag = select(3, GetNetStats()) / 1000
for i, v in ipairs(mod.timers) do
if v.id == id then
v:Start(totalTime, ...)
v:Update(totalTime - timeLeft + lag, totalTime, ...)
end
end
end
end
end
do
local spamProtection = 0
function DBM:SendTimers(target)
if GetTime() - spamProtection < 0.4 then
return
end
spamProtection = GetTime()
if UnitInBattleground("player") then
DBM:SendBGTimers(target)
end
if #inCombat < 1 then return end
local mod
for i, v in ipairs(inCombat) do
mod = not v.isCustomMod and v
end
mod = mod or inCombat[1]
self:SendCombatInfo(mod, target)
self:SendTimerInfo(mod, target)
end
end
function DBM:SendBGTimers(target)
local mod
if IsActiveBattlefieldArena() then
mod = self:GetModByName("Arenas")
else
-- FIXME: this doesn't work for non-english clients
local zone = GetRealZoneText():gsub(" ", "")
mod = self:GetModByName(zone)
end
if mod and mod.timers then
self:SendTimerInfo(mod, target)
end
end
function DBM:SendCombatInfo(mod, target)
return SendAddonMessage("DBMv4-CombatInfo", ("%s\t%s"):format(mod.id, GetTime() - mod.combatInfo.pull), "WHISPER", target)
end
function DBM:SendTimerInfo(mod, target)
for i, v in ipairs(mod.timers) do
for _, uId in ipairs(v.startedTimers) do
local elapsed, totalTime, timeLeft
if select("#", string.split("\t", uId)) > 1 then
elapsed, totalTime = v:GetTime(select(2, string.split("\t", uId)))
else
elapsed, totalTime = v:GetTime()
end
timeLeft = totalTime - elapsed
if timeLeft > 0 and totalTime > 0 then
SendAddonMessage("DBMv4-TimerInfo", ("%s\t%s\t%s\t%s"):format(mod.id, timeLeft, totalTime, uId), "WHISPER", target)
end
end
end
end
do
local function requestTimers()
local uId = ((GetNumRaidMembers() == 0) and "party") or "raid"
for i = 0, math.max(GetNumRaidMembers(), GetNumPartyMembers()) do
local id = (i == 0 and "player") or uId..i
if UnitAffectingCombat(id) and not UnitIsDeadOrGhost(id) then
DBM:RequestTimers()
break
end
end
end
function DBM:PLAYER_ENTERING_WORLD()
if #inCombat == 0 then
DBM:Schedule(0, requestTimers)
end
self:LFG_UPDATE()
-- self:Schedule(10, function() if not DBM.Options.HelpMessageShown then DBM.Options.HelpMessageShown = true DBM:AddMsg(DBM_CORE_NEED_SUPPORT) end end)
end
end
------------------------------------
-- Auto-respond/Status whispers --
------------------------------------
do
local function getNumAlivePlayers()
local alive = 0
if GetNumRaidMembers() > 0 then
for i = 1, GetNumRaidMembers() do
alive = alive + ((UnitIsDeadOrGhost("raid"..i) and 0) or 1)
end
else
alive = (UnitIsDeadOrGhost("player") and 0) or 1
for i = 1, GetNumPartyMembers() do
alive = alive + ((UnitIsDeadOrGhost("party"..i) and 0) or 1)
end
end
return alive
end
local function isOnSameServer(presenceId)
local toonID, client = select(5, BNGetFriendInfoByID(presenceId))
if client ~= "WoW" then
return false
end
return GetRealmName() == select(4, BNGetToonInfo(toonID))
end
-- sender is a presenceId for real id messages, a character name otherwise
local function onWhisper(msg, sender, isRealIdMessage)
if msg == "status" and #inCombat > 0 and DBM.Options.StatusEnabled then
local mod
for i, v in ipairs(inCombat) do
mod = not v.isCustomMod and v
end
mod = mod or inCombat[1]
sendWhisper(sender, chatPrefix..DBM_CORE_STATUS_WHISPER:format((mod.combatInfo.name or ""), mod:GetHP() or "unknown", getNumAlivePlayers(), math.max(GetNumRaidMembers(), GetNumPartyMembers() + 1)))
elseif #inCombat > 0 and DBM.Options.AutoRespond and
(isRealIdMessage and (not isOnSameServer(sender) or DBM:GetRaidUnitId((select(4, BNGetFriendInfoByID(sender)))) == "none") or not isRealIdMessage and DBM:GetRaidUnitId(sender) == "none") then
local mod
for i, v in ipairs(inCombat) do
mod = not v.isCustomMod and v
end
mod = mod or inCombat[1]
if not autoRespondSpam[sender] then
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)))
DBM:AddMsg(DBM_CORE_AUTO_RESPONDED)
end
autoRespondSpam[sender] = true
end
end
function DBM:CHAT_MSG_WHISPER(msg, name)
return onWhisper(msg, name, false)
end
function DBM:CHAT_MSG_BN_WHISPER(msg, ...)
local presenceId = select(12, ...) -- srsly?
return onWhisper(msg, presenceId, true)
end
end
-------------------
-- Chat Filter --
-------------------
do
local function filterOutgoing(self, event, ...)
local msg = ...
if not msg and self then -- compatibility mode!
-- we also check if self exists to prevent a possible freeze if the function is called without arguments at all
-- as this would be even worse than the issue with missing whisper messages ;)
return filterOutgoing(nil, nil, self, event)
end
return msg:sub(1, chatPrefix:len()) == chatPrefix or msg:sub(1, chatPrefixShort:len()) == chatPrefixShort, ...
end
local function filterIncoming(self, event, ...)
local msg = ...
if not msg and self then -- compatibility mode!
return filterIncoming(nil, nil, self, event)
end
if DBM.Options.SpamBlockBossWhispers then
return #inCombat > 0 and (msg == "status" or msg:sub(1, chatPrefix:len()) == chatPrefix or msg:sub(1, chatPrefixShort:len()) == chatPrefixShort), ...
else
return msg == "status" and #inCombat > 0, ...
end
end
local function filterRaidWarning(self, event, ...)
local msg = ...
if not msg and self then -- compatibility mode!
return filterRaidWarning(nil, nil, self, event)
end
return DBM.Options.SpamBlockRaidWarning and type(msg) == "string" and (not not msg:match("^%s*%*%*%*")), ...
end
ChatFrame_AddMessageEventFilter("CHAT_MSG_WHISPER_INFORM", filterOutgoing)
ChatFrame_AddMessageEventFilter("CHAT_MSG_BN_WHISPER_INFORM", filterOutgoing)
ChatFrame_AddMessageEventFilter("CHAT_MSG_WHISPER", filterIncoming)
ChatFrame_AddMessageEventFilter("CHAT_MSG_BN_WHISPER", filterIncoming)
ChatFrame_AddMessageEventFilter("CHAT_MSG_RAID_WARNING", filterRaidWarning)
ChatFrame_AddMessageEventFilter("CHAT_MSG_PARTY", filterRaidWarning)
ChatFrame_AddMessageEventFilter("CHAT_MSG_PARTY_LEADER", filterRaidWarning)
end
do
local old = RaidWarningFrame:GetScript("OnEvent")
RaidWarningFrame:SetScript("OnEvent", function(self, event, msg, ...)
if DBM.Options.SpamBlockRaidWarning and msg:find("%*%*%* .* %*%*%*") then
return
end
return old(self, event, msg, ...)
end)
end
do
local old = RaidBossEmoteFrame:GetScript("OnEvent")
RaidBossEmoteFrame:SetScript("OnEvent", function(...)
if DBM.Options.HideBossEmoteFrame and #inCombat > 0 then
return
end
return old(...)
end)
end
--------------------------
-- Enable/Disable DBM --
--------------------------
function DBM:Disable()
unschedule()
self.Options.Enabled = false
end
function DBM:Enable()
self.Options.Enabled = true
end
function DBM:IsEnabled()
return self.Options.Enabled
end
-----------------------
-- Misc. Functions --
-----------------------
function DBM:AddMsg(text, prefix)
prefix = prefix or (self.localization and self.localization.general.name) or "Deadly Boss Mods"
DEFAULT_CHAT_FRAME:AddMessage(("|cffff7d0a<|r|cffffd200%s|r|cffff7d0a>|r %s"):format(tostring(prefix), tostring(text)), 0.41, 0.8, 0.94)
end
do
local testMod
local testWarning1, testWarning2, testWarning3
-- local testTimer
local testSpecialWarning
local timerTestBar, timerPewPewPew, timerEvilSpell, timerBoom
function DBM:DemoMode()
if not testMod then
testMod = DBM:NewMod("TestMod", "DBM-PvP") -- temp fix, as it requires a modId
testWarning1 = testMod:NewAnnounce("%s", 1, "Interface\\Icons\\Spell_Nature_WispSplode")
testWarning2 = testMod:NewAnnounce("%s", 2, "Interface\\Icons\\Spell_Shadow_ShadesOfDarkness")
testWarning3 = testMod:NewAnnounce("%s", 3, "Interface\\Icons\\Spell_Fire_SelfDestruct")
-- testTimer = testMod:NewTimer(20, "%s", "%%s")
timerTestBar = testMod:NewTimer(10, "Test Bar", "Interface\\Icons\\Spell_Nature_WispSplode")
timerPewPewPew = testMod:NewTimer(20, "Pew Pew Pew...", "Interface\\Icons\\Spell_Nature_Starfall")
timerEvilSpell = testMod:NewTimer(43, "Evil Spell", "Interface\\Icons\\Spell_Shadow_ShadesOfDarkness")
timerBoom = testMod:NewTimer(60, "Boom!", "Interface\\Icons\\Spell_Fire_SelfDestruct")
testSpecialWarning = testMod:NewSpecialWarning("%s")
end
timerTestBar:Start()
timerPewPewPew:Start()
timerEvilSpell:Start()
timerBoom:Start()
testWarning1:Cancel()
testWarning2:Cancel()
testWarning3:Cancel()
testSpecialWarning:Cancel()
testWarning1:Show("Test-mode started...")
testWarning1:Schedule(10, "Test bar expired!")
testWarning3:Schedule(20, "Pew Pew Laser Owl!")
testWarning2:Schedule(38, "Evil Spell in 5 sec!")
testWarning2:Schedule(43, "Evil Spell!")
testWarning3:Schedule(50, "Boom in 10 sec!")
testWarning1:Schedule(62, "Test-mode finished!")
testSpecialWarning:Schedule(60, "Boom!")
end
end
DBM.Bars:SetAnnounceHook(function(bar)
local prefix
if bar.color and bar.color.r == 1 and bar.color.g == 0 and bar.color.b == 0 then
prefix = DBM_CORE_HORDE
elseif bar.color and bar.color.r == 0 and bar.color.g == 0 and bar.color.b == 1 then
prefix = DBM_CORE_ALLIANCE
end
if prefix then
return ("%s: %s %d:%02d"):format(prefix, _G[bar.frame:GetName().."BarName"]:GetText(), math.floor(bar.timer / 60), bar.timer % 60)
end
end)
function DBM:Capitalize(str)
local firstByte = str:byte(1, 1)
local numBytes = 1
if firstByte >= 0xF0 then -- firstByte & 0b11110000
numBytes = 4
elseif firstByte >= 0xE0 then -- firstByte & 0b11100000
numBytes = 3
elseif firstByte >= 0xC0 then -- firstByte & 0b11000000
numBytes = 2
end
return str:sub(1, numBytes):upper()..str:sub(numBytes + 1):lower()
end
-- An anti spam function to throttle spammy events (e.g. SPELL_AURA_APPLIED on all group members)
-- @param time the time to wait between two events (optional, default 2.5 seconds)
-- @param id the id to distinguish different events (optional, only necessary if your mod keeps track of two different spam events at the same time)
function DBM:AntiSpam(times, id)
if GetTime() - (id and (self["lastAntiSpam" .. tostring(id)] or 0) or self.lastAntiSpam or 0) > (times or 2.5) then
if id then
self["lastAntiSpam" .. tostring(id)] = GetTime()
else
self.lastAntiSpam = GetTime()
end
return true
else
return false
end
end
-----------------
-- Map Sizes --
-----------------
DBM.MapSizes = {}
function DBM:RegisterMapSize(zone, ...)
if not DBM.MapSizes[zone] then
DBM.MapSizes[zone] = {}
end
local zone = DBM.MapSizes[zone]
for i = 1, select("#", ...), 3 do
local level, width, height = select(i, ...)
zone[level] = {width, height}
end
end
--------------------------
-- Boss Mod Prototype --
--------------------------
local bossModPrototype = {}
----------------------------
-- Boss Mod Constructor --
----------------------------
do
local modsById = setmetatable({}, {__mode = "v"})
local mt = {__index = bossModPrototype}
function DBM:NewMod(name, modId, modSubTab)
if modsById[name] then error("DBM:NewMod(): Mod names are used as IDs and must therefore be unique.", 2) end
local obj = setmetatable(
{
Options = {
Enabled = true,
Announce = false,
},
subTab = modSubTab,
optionCategories = {
},
categorySort = {},
id = name,
announces = {},
specwarns = {},
vb = {}, -- variables table, used by details to check phase
timers = {},
modId = modId,
revision = 0,
localization = self:GetModLocalization(name)
},
mt
)
for i, v in ipairs(self.AddOns) do
if v.modId == modId then
obj.addon = v
break
end
end
if obj.localization.general.name == "name" then obj.localization.general.name = name end
table.insert(self.Mods, obj)
modsById[name] = obj
obj:AddBoolOption("HealthFrame", false, "misc")
obj:SetZone()
return obj
end
function DBM:GetModByName(name)
return modsById[name]
end
end
-----------------------
-- General Methods --
-----------------------
bossModPrototype.RegisterEvents = DBM.RegisterEvents
bossModPrototype.UnregisterAllEvents = DBM.UnregisterAllEvents
bossModPrototype.AddMsg = DBM.AddMsg
function bossModPrototype:SetZone(...)
if select("#", ...) == 0 then
if self.addon.zone and #self.addon.zone > 0 and self.addon.zoneId and #self.addon.zoneId > 0 then
self.zones = {}
for i, v in ipairs(self.addon.zone) do
self.zones[#self.zones + 1] = v
end
for i, v in ipairs(self.addon.zoneId) do
self.zones[#self.zones + 1] = v
end
else
self.zones = self.addon.zone and #self.addon.zone > 0 and self.addon.zone or self.addon.zoneId and #self.addon.zoneId > 0 and self.addon.zoneId or {}
end
elseif select(1, ...) ~= DBM_DISABLE_ZONE_DETECTION then
self.zones = {...}
else -- disable zone detection
self.zones = nil
end
end
function bossModPrototype:SetCreatureID(...)
self.creatureId = ...
if select("#", ...) > 1 then
self.multiMobPullDetection = {...}
if self.combatInfo then
self.combatInfo.multiMobPullDetection = self.multiMobPullDetection
end
end
end
function bossModPrototype:Toggle()
if self.Options.Enabled then
self:DisableMod()
else
self:EnableMod()
end
end
function bossModPrototype:EnableMod()
self.Options.Enabled = true
end
function bossModPrototype:DisableMod()
self:Stop()
self.Options.Enabled = false
end
function bossModPrototype:RegisterOnUpdateHandler(func, interval)
if type(func) ~= "function" then return end
self.elapsed = 0
self.updateInterval = interval or 0
updateFunctions[self] = func
end
function bossModPrototype:SetRevision(revision)
self.revision = revision
end
function bossModPrototype:SendWhisper(msg, target)
return not DBM.Options.DontSendBossWhispers and sendWhisper(target, chatPrefixShort..msg)
end
function bossModPrototype:GetUnitCreatureId(uId)
local guid = UnitGUID(uId)
return (guid and tonumber(guid:sub(9, 12), 16)) or 0
end
function bossModPrototype:GetCIDFromGUID(guid)
return (guid and tonumber(guid:sub(9, 12), 16)) or 0
end
function bossModPrototype:GetBossTarget(cid)
cid = cid or self.creatureId
for i = 1, GetNumRaidMembers() do
if self:GetUnitCreatureId("raid"..i.."target") == cid then
return UnitName("raid"..i.."targettarget"), "raid"..i.."targettarget"
elseif self:GetUnitCreatureId("focus") == cid then -- we check our own focus frame, maybe the boss is there ;)
return UnitName("focustarget"), "focustarget"
end
end
end
function bossModPrototype:GetThreatTarget(cid)
cid = cid or self.creatureId
for i = 1, GetNumRaidMembers() do
if self:GetUnitCreatureId("raid"..i.."target") == cid then
for x = 1, GetNumRaidMembers() do
if UnitDetailedThreatSituation("raid"..x, "raid"..i.."target") == 1 then
return "raid"..x
end
end
end
end
end
function bossModPrototype:Stop(cid)
for i, v in ipairs(self.timers) do
v:Stop()
end
self:Unschedule()
end
-- hard coded party-mod support, yay :)
-- returns heroic for old instances that do not have a heroic mode (Naxx, Ulduar...)
function bossModPrototype:GetDifficulty()
local _, instanceType, difficulty, _, _, playerDifficulty, isDynamicInstance = GetInstanceInfo()
if instanceType == "raid" and isDynamicInstance then -- "new" instance (ICC)
if (difficulty == 1) then -- 10 men
return playerDifficulty == 0 and "normal10" or playerDifficulty == 1 and "heroic10" or "unknown"
elseif (difficulty == 2) then -- 25 men
return playerDifficulty == 0 and "normal25" or playerDifficulty == 1 and "heroic25" or "unknown"
elseif (difficulty == 3) then
return "heroic10";
elseif (difficulty == 4) then
return "heroic25";
end
else -- support for "old" instances
if GetInstanceDifficulty() == 1 then
return (self.modId == "DBM-Party-WotLK" or self.modId == "DBM-Party-BC") and "normal5" or
self.hasHeroic and "normal10" or "heroic10"
elseif GetInstanceDifficulty() == 2 then
return (self.modId == "DBM-Party-WotLK" or self.modId == "DBM-Party-BC") and "heroic5" or
self.hasHeroic and "normal25" or "heroic25"
elseif GetInstanceDifficulty() == 3 then
return "heroic10"
elseif GetInstanceDifficulty() == 4 then
return "heroic25"
end
end
end
function bossModPrototype:IsDifficulty(...)
local diff = self:GetDifficulty()
for i = 1, select("#", ...) do
if diff == select(i, ...) then
return true
end
end
return false
end
function bossModPrototype:SetUsedIcons(...)
self.usedIcons = {}
for i = 1, select("#", ...) do
self.usedIcons[select(i, ...)] = true
end
end
function bossModPrototype:LatencyCheck()
return select(3, GetNetStats()) < DBM.Options.LatencyThreshold
end
local function getTalentpointsSpent(spellID)
local spellName = GetSpellInfo(spellID)
for tabIndex=1, GetNumTalentTabs() do
for talentID=1, GetNumTalents(tabIndex) do
local name, _, _, _, spent = GetTalentInfo(tabIndex, talentID)
if(name == spellName) then
return spent
end
end
end
return 0
end
function bossModPrototype:IsMelee()
return select(2, UnitClass("player")) == "ROGUE"
or select(2, UnitClass("player")) == "WARRIOR"
or select(2, UnitClass("player")) == "DEATHKNIGHT"
or (select(2, UnitClass("player")) == "PALADIN" and select(3, GetTalentTabInfo(1)) < 51)
or (select(2, UnitClass("player")) == "SHAMAN" and select(3, GetTalentTabInfo(2)) >= 51)
or (select(2, UnitClass("player")) == "DRUID" and select(3, GetTalentTabInfo(2)) >= 51)
end
function bossModPrototype:IsRanged()
return select(2, UnitClass("player")) == "MAGE"
or select(2, UnitClass("player")) == "HUNTER"
or select(2, UnitClass("player")) == "WARLOCK"
or select(2, UnitClass("player")) == "PRIEST"
or (select(2, UnitClass("player")) == "PALADIN" and select(3, GetTalentTabInfo(1)) >= 51)
or (select(2, UnitClass("player")) == "SHAMAN" and select(3, GetTalentTabInfo(2)) < 51)
or (select(2, UnitClass("player")) == "DRUID" and select(3, GetTalentTabInfo(2)) < 51)
end
function bossModPrototype:IsPhysical()
return self:IsMelee() or select(2, UnitClass("player")) == "HUNTER"
end
function bossModPrototype:CanRemoveEnrage()
return select(2, UnitClass("player")) == "HUNTER" or select(2, UnitClass("player")) == "ROGUE"
end
local function IsDeathKnightTank()
-- idea taken from addon 'ElitistJerks'
local tankTalents = (getTalentpointsSpent(16271) >= 5 and 1 or 0) + -- Anticipation
(getTalentpointsSpent(49042) >= 5 and 1 or 0) + -- Toughness
(getTalentpointsSpent(55225) >= 5 and 1 or 0) -- Blade Barrier
return tankTalents >= 2
end
local function IsDruidTank()
-- idea taken from addon 'ElitistJerks'
local tankTalents = (getTalentpointsSpent(57881) >= 2 and 1 or 0) + -- Natural Reaction
(getTalentpointsSpent(16929) >= 3 and 1 or 0) + -- Thick Hide
(getTalentpointsSpent(61336) >= 1 and 1 or 0) + -- Survival Instincts
(getTalentpointsSpent(57877) >= 3 and 1 or 0) -- Protector of the Pack
return tankTalents >= 3
end
function bossModPrototype:IsTank()
return (select(2, UnitClass("player")) == "WARRIOR" and select(3, GetTalentTabInfo(3)) >= 51)
or (select(2, UnitClass("player")) == "DEATHKNIGHT" and IsDeathKnightTank())
or (select(2, UnitClass("player")) == "PALADIN" and select(3, GetTalentTabInfo(2)) >= 51)
or (select(2, UnitClass("player")) == "DRUID" and select(3, GetTalentTabInfo(2)) >= 51 and IsDruidTank())
end
function bossModPrototype:IsHealer()
return (select(2, UnitClass("player")) == "PALADIN" and select(3, GetTalentTabInfo(1)) >= 51)
or (select(2, UnitClass("player")) == "SHAMAN" and select(3, GetTalentTabInfo(3)) >= 51)
or (select(2, UnitClass("player")) == "DRUID" and select(3, GetTalentTabInfo(3)) >= 51)
or (select(2, UnitClass("player")) == "PRIEST" and select(3, GetTalentTabInfo(3)) < 51)
end
-------------------------
-- Boss Health Frame --
-------------------------
function bossModPrototype:SetBossHealthInfo(...)
self.bossHealthInfo = {...}
end
-----------------------
-- Announce Object --
-----------------------
do
local textureCode = " |T%s:12:12|t "
local textureExp = " |T(%S+):12:12|t "
local announcePrototype = {}
local mt = {__index = announcePrototype}
local cachedColorFunctions = setmetatable({}, {__mode = "kv"})
function announcePrototype:Show(...) -- todo: reduce amount of unneeded strings
if not self.option or self.mod.Options[self.option] then
if self.mod.Options.Announce and not DBM.Options.DontSendBossAnnounces and (DBM:GetRaidRank() > 0 or (GetNumRaidMembers() == 0 and GetNumPartyMembers() >= 1)) then
local message = pformat(self.text, ...)
message = message:gsub("|3%-%d%((.-)%)", "%1") -- for |3-id(text) encoding in russian localization
SendChatMessage(("*** %s ***"):format(message), GetNumRaidMembers() > 0 and "RAID_WARNING" or "PARTY")
end
if DBM.Options.DontShowBossAnnounces then return end -- don't show the announces if the spam filter option is set
local colorCode = ("|cff%.2x%.2x%.2x"):format(self.color.r * 255, self.color.g * 255, self.color.b * 255)
local text = ("%s%s%s|r%s"):format(
(DBM.Options.WarningIconLeft and self.icon and textureCode:format(self.icon)) or "",
colorCode,
pformat(self.text, ...),
(DBM.Options.WarningIconRight and self.icon and textureCode:format(self.icon)) or ""
)
if not cachedColorFunctions[self.color] then
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)
cachedColorFunctions[color] = function(cap)
cap = cap:sub(2, -2)
if DBM:GetRaidClass(cap) then
local playerColor = RAID_CLASS_COLORS[DBM:GetRaidClass(cap)] or color
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)
end
return cap
end
end
text = text:gsub(">.-<", cachedColorFunctions[self.color])
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)
if DBM.Options.ShowWarningsInChat then
text = text:gsub(textureExp, "") -- textures @ chat frame can (and will) distort the font if using certain combinations of UI scale, resolution and font size
if DBM.Options.ShowFakedRaidWarnings then
for i = 1, select("#", GetFramesRegisteredForEvent("CHAT_MSG_RAID_WARNING")) do
local frame = select(i, GetFramesRegisteredForEvent("CHAT_MSG_RAID_WARNING"))
if frame ~= RaidWarningFrame and frame:GetScript("OnEvent") then
frame:GetScript("OnEvent")(frame, "CHAT_MSG_RAID_WARNING", text, UnitName("player"), GetDefaultLanguage("player"), "", UnitName("player"), "", 0, 0, "", 0, 99, "")
end
end
else
self.mod:AddMsg(text, nil)
end
end
PlaySoundFile(DBM.Options.RaidWarningSound)
fireEvent("DBM_Announce", message, self.icon, self.type, self.spellId, self.mod.id, false)
end
end
function announcePrototype:Schedule(t, ...)
return schedule(t, self.Show, self.mod, self, ...)
end
function announcePrototype:Cancel(...)
return unschedule(self.Show, self.mod, self, ...)
end
-- old constructor (no auto-localize)
function bossModPrototype:NewAnnounce(text, color, icon, optionDefault, optionName)
local obj = setmetatable(
{
text = self.localization.warnings[text],
color = DBM.Options.WarningColors[color or 1] or DBM.Options.WarningColors[1],
option = optionName or text,
mod = self,
icon = (type(icon) == "number" and select(3, GetSpellInfo(icon))) or icon,
},
mt
)
if optionName == false then
obj.option = nil
else
self:AddBoolOption(optionName or text, optionDefault, "announce")
end
table.insert(self.announces, obj)
return obj
end
-- new constructor (auto-localized warnings and options, yay!)
local function newAnnounce(self, announceType, spellId, color, icon, optionDefault, optionName, castTime, preWarnTime)
spellName = GetSpellInfo(spellId) or "unknown"
icon = icon or spellId
local text
if announceType == "cast" then
local spellHaste = select(7, GetSpellInfo(53142)) / 10000 -- 53142 = Dalaran Portal, should have 10000 ms cast time
local timer = (select(7, GetSpellInfo(spellId)) or 1000) / spellHaste
text = DBM_CORE_AUTO_ANNOUNCE_TEXTS[announceType]:format(spellName, castTime or (timer / 1000))
elseif announceType == "prewarn" then
if type(preWarnTime) == "string" then
text = DBM_CORE_AUTO_ANNOUNCE_TEXTS[announceType]:format(spellName, preWarnTime)
else
text = DBM_CORE_AUTO_ANNOUNCE_TEXTS[announceType]:format(spellName, DBM_CORE_SEC_FMT:format(preWarnTime or 5))
end
elseif announceType == "phase" then
text = DBM_CORE_AUTO_ANNOUNCE_TEXTS[announceType]:format(spellId)
else
text = DBM_CORE_AUTO_ANNOUNCE_TEXTS[announceType]:format(spellName)
end
local obj = setmetatable( -- todo: fix duplicate code
{
text = text,
announceType = announceType,
color = DBM.Options.WarningColors[color or 1] or DBM.Options.WarningColors[1],
option = optionName or text,
mod = self,
icon = (type(icon) == "number" and select(3, GetSpellInfo(icon))) or icon,
},
mt
)
if optionName == false then
obj.option = nil
else
self:AddBoolOption(optionName or text, optionDefault, "announce")
end
table.insert(self.announces, obj)
self.localization.options[text] = DBM_CORE_AUTO_ANNOUNCE_OPTIONS[announceType]:format(spellId, spellName)
return obj
end
function bossModPrototype:NewTargetAnnounce(spellId, color, ...)
return newAnnounce(self, "target", spellId, color or 2, ...)
end
function bossModPrototype:NewSpellAnnounce(spellId, color, ...)
return newAnnounce(self, "spell", spellId, color or 3, ...)
end
function bossModPrototype:NewCastAnnounce(spellId, color, castTime, icon, optionDefault, optionName)
return newAnnounce(self, "cast", spellId, color or 3, icon, optionDefault, optionName, castTime)
end
function bossModPrototype:NewSoonAnnounce(spellId, color, ...)
return newAnnounce(self, "soon", spellId, color or 1, ...)
end
function bossModPrototype:NewPreWarnAnnounce(spellId, time, color, icon, optionDefault, optionName)
return newAnnounce(self, "prewarn", spellId, color or 1, icon, optionDefault, optionName, nil, time)
end
function bossModPrototype:NewPhaseAnnounce(phase, color, icon, ...)
return newAnnounce(self, "phase", phase, color or 1, icon or "Interface\\Icons\\Spell_Nature_WispSplode", ...)
end
function bossModPrototype:NewInterruptAnnounce(spellId, color, ...)
return newAnnounce(self, "interrupt", spellId, color or 3, ...)
end
end
--------------------
-- Sound Object --
--------------------
do
local soundPrototype = {}
local mt = { __index = soundPrototype }
function bossModPrototype:NewSound(spellId, optionName, optionDefault)
self.numSounds = self.numSounds and self.numSounds + 1 or 1
local obj = setmetatable(
{
option = optionName or DBM_CORE_AUTO_SOUND_OPTION_TEXT:format(spellId),
mod = self,
},
mt
)
if optionName == false then
obj.option = nil
else
self:AddBoolOption(obj.option, optionDefault, "misc")
end
return obj
end
bossModPrototype.NewRunAwaySound = bossModPrototype.NewSound
function soundPrototype:Play(file)
if not self.option or self.mod.Options[self.option] then
PlaySoundFile(file or "Sound\\Creature\\HoodWolf\\HoodWolfTransformPlayer01.wav")
end
end
function soundPrototype:Schedule(t, ...)
return schedule(t, self.Play, self.mod, self, ...)
end
function soundPrototype:Cancel(...)
return unschedule(self.Play, self.mod, self, ...)
end
end
--------------------
-- Yell Object --
--------------------
do
local yellPrototype = {}
local mt = { __index = yellPrototype }
local function newYell(self, yellType, spellId, yellText, optionDefault, optionName, chatType)
if not spellId and not yellText then
error("NewYell: you must provide either spellId or yellText", 2)
return
end
-- if type(spellId) == "string" and spellId:match("OptionVersion") then
-- print("newYell for: "..yellText.." is using OptionVersion hack. This is depricated")
-- return
-- end
-- local optionVersion
-- if type(optionName) == "number" then
-- optionVersion = optionName
-- optionName = nil
-- end
local displayText
if not yellText then
-- if type(spellId) == "string" and spellId:match("ej%d+") then
-- displayText = DBM_CORE_AUTO_YELL_ANNOUNCE_TEXT[yellType]:format(EJ_GetSectionInfo(string.sub(spellId, 3)) or DBM_CORE_UNKNOWN)
-- else
displayText = DBM_CORE_AUTO_YELL_ANNOUNCE_TEXT[yellType]:format(GetSpellInfo(spellId) or DBM_CORE_UNKNOWN)
-- end
else
displayText = DBM_CORE_AUTO_YELL_ANNOUNCE_TEXT[yellType]:format(GetSpellInfo(spellId), yellText)
end
--Passed spellid as yellText.
--Auto localize spelltext using yellText instead
-- if yellText then --and type(yellText) == "number"
-- end
local obj = setmetatable(
{
text = displayText or yellText,
mod = self,
chatType = chatType,
yellType = yellType
},
mt
)
if optionName then
obj.option = optionName
self:AddBoolOption(obj.option, optionDefault, "misc")
elseif not (optionName == false) then
obj.option = "Yell"..(spellId or yellText)..(yellType ~= "yell" and yellType or "")..(optionVersion or "")
self:AddBoolOption(obj.option, optionDefault, "misc")
self.localization.options[obj.option] = DBM_CORE_AUTO_YELL_OPTION[yellType]:format(spellId)
end
return obj
end
function yellPrototype:Yell(...)
if self.yellType and self.yellType == "position" then return end
if not self.option or self.mod.Options[self.option] then
if self.yellType == "combo" then
SendChatMessage(pformat(self.text, ...), self.chatType or "YELL")
else
SendChatMessage(pformat(self.text, ...), self.chatType or "SAY")
end
end
end
yellPrototype.Show = yellPrototype.Yell
function yellPrototype:Schedule(t, ...)
return schedule(t, self.Yell, self.mod, self, ...)
end
function yellPrototype:Countdown(time, numAnnounces, ...)
scheduleCountdown(time, numAnnounces, self.Yell, self.mod, self, ...)
end
function yellPrototype:Cancel(...)
return unschedule(self.Yell, self.mod, self, ...)
end
function bossModPrototype:NewYell(...)
return newYell(self, "yell", ...)
end
function bossModPrototype:NewShortYell(...)
return newYell(self, "shortyell", ...)
end
function bossModPrototype:NewCountYell(...)
return newYell(self, "count", ...)
end
function bossModPrototype:NewFadesYell(...)
return newYell(self, "fade", ...)
end
function bossModPrototype:NewShortFadesYell(...)
return newYell(self, "shortfade", ...)
end
function bossModPrototype:NewIconFadesYell(...)
return newYell(self, "iconfade", ...)
end
function bossModPrototype:NewPosYell(...)
return newYell(self, "position", ...)
end
function bossModPrototype:NewComboYell(...)
return newYell(self, "combo", ...)
end
end
------------------------------
-- Special Warning Object --
------------------------------
do
local frame = CreateFrame("Frame", nil, UIParent)
local font = frame:CreateFontString(nil, "OVERLAY", "ZoneTextFont")
frame:SetMovable(1)
frame:SetWidth(1)
frame:SetHeight(1)
frame:SetFrameStrata("HIGH")
frame:SetClampedToScreen()
frame:Hide()
font:SetWidth(1024)
font:SetHeight(0)
font:SetPoint("CENTER", 0, 0)
local moving
local specialWarningPrototype = {}
local mt = {__index = specialWarningPrototype}
function DBM:UpdateSpecialWarningOptions()
frame:ClearAllPoints()
frame:SetPoint(DBM.Options.SpecialWarningPoint, UIParent, DBM.Options.SpecialWarningPoint, DBM.Options.SpecialWarningX, DBM.Options.SpecialWarningY)
font:SetFont(DBM.Options.SpecialWarningFont, DBM.Options.SpecialWarningFontSize, "THICKOUTLINE")
font:SetTextColor(unpack(DBM.Options.SpecialWarningFontColor))
end
local shakeFrame = CreateFrame("Frame")
shakeFrame:SetScript("OnUpdate", function(self, elapsed)
self.timer = self.timer - elapsed
end)
shakeFrame:Hide()
frame:SetScript("OnUpdate", function(self, elapsed)
self.timer = self.timer - elapsed
if self.timer >= 3 and self.timer <= 4 then
LowHealthFrame:SetAlpha(self.timer - 3)
elseif self.timer <= 2 then
frame:SetAlpha(self.timer/2)
elseif self.timer <= 0 then
frame:Hide()
end
end)
function specialWarningPrototype:Show(...)
if DBM.Options.ShowSpecialWarnings and (not self.option or self.mod.Options[self.option]) and not moving and frame then
font:SetText(pformat(self.text, ...))
LowHealthFrame:Show()
LowHealthFrame:SetAlpha(1)
frame:Show()
frame:SetAlpha(1)
frame.timer = 5
if self.sound then
PlaySoundFile(DBM.Options.SpecialWarningSound)
end
fireEvent("DBM_Announce", message, self.icon, self.type, self.spellId, self.mod.id, false)
end
end
function specialWarningPrototype:Schedule(t, ...)
return schedule(t, self.Show, self.mod, self, ...)
end
function specialWarningPrototype:Cancel(t, ...)
return unschedule(self.Show, self.mod, self, ...)
end
function bossModPrototype:NewSpecialWarning(text, optionDefault, optionName, noSound, runSound)
local obj = setmetatable(
{
text = self.localization.warnings[text],
option = optionName or text,
mod = self,
sound = not noSound,
},
mt
)
if optionName == false then
obj.option = nil
else
self:AddBoolOption(optionName or text, optionDefault, "announce")
end
table.insert(self.specwarns, obj)
return obj
end
local function newSpecialWarning(self, announceType, spellId, stacks, optionDefault, optionName, noSound, runSound)
spellName = GetSpellInfo(spellId) or "unknown"
local text = DBM_CORE_AUTO_SPEC_WARN_TEXTS[announceType]:format(spellName)
local obj = setmetatable( -- todo: fix duplicate code
{
text = text,
announceType = announceType,
option = optionName or text,
mod = self,
sound = not noSound,
},
mt
)
if optionName == false then
obj.option = nil
else
self:AddBoolOption(optionName or text, optionDefault, "announce") -- todo cleanup core code from that indexing type using options[text] is very bad!!! ;)
end
table.insert(self.specwarns, obj)
if announceType == "stack" then
self.localization.options[text] = DBM_CORE_AUTO_SPEC_WARN_OPTIONS[announceType]:format(stacks or 3, spellId)
else
self.localization.options[text] = DBM_CORE_AUTO_SPEC_WARN_OPTIONS[announceType]:format(spellId)
end
return obj
end
function bossModPrototype:NewSpecialWarningSpell(text, optionDefault, ...)
return newSpecialWarning(self, "spell", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningDispel(text, optionDefault, ...)
return newSpecialWarning(self, "dispel", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningInterupt(text, optionDefault, ...)
return newSpecialWarning(self, "interupt", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningYou(text, optionDefault, ...)
return newSpecialWarning(self, "you", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningTarget(text, optionDefault, ...)
return newSpecialWarning(self, "target", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningClose(text, optionDefault, ...)
return newSpecialWarning(self, "close", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningMove(text, optionDefault, ...)
return newSpecialWarning(self, "move", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningRun(text, optionDefault, ...)
return newSpecialWarning(self, "run", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningCast(text, optionDefault, ...)
return newSpecialWarning(self, "cast", text, nil, optionDefault, ...)
end
function bossModPrototype:NewSpecialWarningStack(text, optionDefault, stacks, ...)
return newSpecialWarning(self, "stack", text, stacks, optionDefault, ...)
end
do
local anchorFrame
local function moveEnd()
moving = false
anchorFrame:Hide()
frame.timer = 1.5 -- fade out
frame:SetFrameStrata("HIGH")
DBM:Unschedule(moveEnd)
DBM.Bars:CancelBar(DBM_CORE_MOVE_SPECIAL_WARNING_BAR)
end
function DBM:MoveSpecialWarning()
if not anchorFrame then
anchorFrame = CreateFrame("Frame", nil, frame)
anchorFrame:SetWidth(32)
anchorFrame:SetHeight(32)
anchorFrame:EnableMouse(true)
anchorFrame:SetPoint("CENTER", 0, -32)
anchorFrame:RegisterForDrag("LeftButton")
anchorFrame:SetClampedToScreen()
anchorFrame:Hide()
local texture = anchorFrame:CreateTexture()
texture:SetTexture("Interface\\Addons\\DBM-GUI\\textures\\dot.blp")
texture:SetPoint("CENTER", anchorFrame, "CENTER", 0, 0)
texture:SetWidth(32)
texture:SetHeight(32)
anchorFrame:SetScript("OnDragStart", function()
frame:StartMoving()
DBM:Unschedule(moveEnd)
DBM.Bars:CancelBar(DBM_CORE_MOVE_SPECIAL_WARNING_BAR)
end)
anchorFrame:SetScript("OnDragStop", function()
frame:StopMovingOrSizing()
local point, _, _, xOfs, yOfs = frame:GetPoint(1)
DBM.Options.SpecialWarningPoint = point
DBM.Options.SpecialWarningX = xOfs
DBM.Options.SpecialWarningY = yOfs
DBM:Schedule(15, moveEnd)
DBM.Bars:CreateBar(15, DBM_CORE_MOVE_SPECIAL_WARNING_BAR)
end)
end
if anchorFrame:IsShown() then
moveEnd()
else
moving = true
anchorFrame:Show()
self:Schedule(15, moveEnd)
DBM.Bars:CreateBar(15, DBM_CORE_MOVE_SPECIAL_WARNING_BAR)
font:SetText(DBM_CORE_MOVE_SPECIAL_WARNING_TEXT)
frame:Show()
frame:SetFrameStrata("TOOLTIP")
frame:SetAlpha(1)
frame.timer = math.huge
end
end
end
local function testWarningEnd()
frame:SetFrameStrata("HIGH")
end
function DBM:ShowTestSpecialWarning(text)
if moving then
return
end
font:SetText(DBM_CORE_MOVE_SPECIAL_WARNING_TEXT)
frame:Show()
frame:SetAlpha(1)
frame:SetFrameStrata("TOOLTIP")
self:Unschedule(testWarningEnd)
self:Schedule(3, testWarningEnd)
frame.timer = 3
end
end
--------------------
-- Timer Object --
--------------------
do
local timerPrototype = {}
local mt = {__index = timerPrototype}
function timerPrototype:Start(timer, ...)
if timer and type(timer) ~= "number" then
return self:Start(nil, timer, ...) -- first argument is optional!
end
if not self.option or self.mod.Options[self.option] then
local timer = timer and ((timer > 0 and timer) or self.timer + timer) or self.timer
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local barGroup = self.mod.barGroup or DBM.Bars;
local bar = barGroup:CreateBar(timer, id, self.icon)
if not bar then
return false, "error" -- creating the timer failed somehow, maybe hit the hard-coded timer limit of 15
end
if self.type and not self.text then
msg = pformat(self.mod:GetLocalizedTimerText(self.type, self.spellId), ...)
else
msg = pformat(self.text, ...)
end
bar:SetText(msg)
if not msg then
msg = "Something went wrong"
end
fireEvent("DBM_TimerStart", id, msg, timer, self.icon, self.type, self.spellId, colorId)
table.insert(self.startedTimers, id)
self.mod:Unschedule(removeEntry, self.startedTimers, id)
self.mod:Schedule(timer, removeEntry, self.startedTimers, id)
self.mod:Schedule(timer, fireEvent, "DBM_Announce", message, self.icon, self.type, self.spellId, self.mod.id, false)
return bar
else
return false, "disabled"
end
end
timerPrototype.Show = timerPrototype.Start
function timerPrototype:Schedule(t, ...)
return schedule(t, self.Start, self.mod, self, ...)
end
function timerPrototype:Unschedule(t, ...)
return unschedule(self.Start, self.mod, self, ...)
end
function timerPrototype:Stop(...)
fireEvent("DBM_Announce", message, self.icon, self.type, self.spellId, self.mod.id, false)
local barGroup = self.mod.barGroup or DBM.Bars;
if select("#", ...) == 0 then
for i = #self.startedTimers, 1, -1 do
fireEvent("DBM_TimerStop", self.startedTimers[i])
barGroup:CancelBar(self.startedTimers[i])
self.startedTimers[i] = nil
end
else
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
for i = #self.startedTimers, 1, -1 do
if self.startedTimers[i] == id then
fireEvent("DBM_TimerStop", id, guid)
barGroup:CancelBar(id)
table.remove(self.startedTimers, i)
end
end
end
end
function timerPrototype:Cancel(...)
self:Stop(...)
self:Unschedule(...)
end
function timerPrototype:GetTime(...)
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local barGroup = self.mod.barGroup or DBM.Bars;
local bar = barGroup:GetBar(id)
return bar and (bar.totalTime - bar.timer) or 0, (bar and bar.totalTime) or 0
end
function timerPrototype:IsStarted(...)
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local barGroup = self.mod.barGroup or DBM.Bars;
local bar = barGroup:GetBar(id)
return bar and true
end
function timerPrototype:SetTimer(timer)
self.timer = timer
end
function timerPrototype:Update(elapsed, totalTime, ...)
if self:GetTime(...) == 0 then
self:Start(totalTime, ...)
end
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local barGroup = self.mod.barGroup or DBM.Bars;
fireEvent("DBM_TimerUpdate", id, elapsed, totalTime)
return barGroup:UpdateBar(id, elapsed, totalTime)
end
function timerPrototype:AddTime(extendAmount, ...)
-- if DBM.Options.DontShowBossTimers then return end
if self:GetTime(...) == 0 then
return self:Start(extendAmount, ...)
else
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local bar = DBM.Bars:GetBar(id)
if bar then
local elapsed, total = (bar.totalTime - bar.timer), bar.totalTime
if elapsed and total then
-- if bar.countdown then --Unused in our 3.3.5 version
-- DBM:Unschedule(playCountSound, id)
-- if not bar.fade then--Don't start countdown voice if it's faded bar
-- local newRemaining = (total+extendAmount) - elapsed
-- playCountdown(id, newRemaining, bar.countdown, bar.countdownMax)--timerId, timer, voice, count
-- DBM:Debug("Updating a countdown after a timer AddTime call for timer ID:"..id)
-- end
-- end
fireEvent("DBM_TimerUpdate", id, elapsed, total+extendAmount)
return DBM.Bars:UpdateBar(id, elapsed, total+extendAmount)
end
end
end
end
function timerPrototype:RemoveTime(reduceAmount, ...)
-- if DBM.Options.DontShowBossTimers then return end
if self:GetTime(...) == 0 then
return--Do nothing
else
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local bar = DBM.Bars:GetBar(id)
if bar then
local elapsed, total = (bar.totalTime - bar.timer), bar.totalTime
if elapsed and total then
local newRemaining = (total-reduceAmount) - elapsed
if newRemaining > 0 then
-- if bar.countdown and newRemaining > 2 then--Unused in our 3.3.5 version
-- DBM:Unschedule(playCountSound, id)
-- if not bar.fade then--Don't start countdown voice if it's faded bar
-- playCountdown(id, newRemaining, bar.countdown, bar.countdownMax)--timerId, timer, voice, count
-- DBM:Debug("Updating a countdown after a timer RemoveTime call for timer ID:"..id)
-- end
-- end
fireEvent("DBM_TimerUpdate", id, elapsed, total-reduceAmount)
return DBM.Bars:UpdateBar(id, elapsed, total-reduceAmount)
else--New remaining less than 0
if bar.countdown then
DBM:Unschedule(playCountSound, id)
end
fireEvent("DBM_TimerStop", id)
return DBM.Bars:CancelBar(id)
end
end
end
end
end
function timerPrototype:UpdateIcon(icon, ...)
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local barGroup = self.mod.barGroup or DBM.Bars;
local bar = barGroup:GetBar(id)
if bar then
return bar:SetIcon((type(icon) == "number" and select(3, GetSpellInfo(icon))) or icon)
end
end
function timerPrototype:UpdateName(name, ...)
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local barGroup = self.mod.barGroup or DBM.Bars;
local bar = barGroup:GetBar(id)
if bar then
return bar:SetText(name)
end
end
function timerPrototype:SetColor(c, ...)
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local barGroup = self.mod.barGroup or DBM.Bars;
local bar = barGroup:GetBar(id)
if bar then
return bar:SetColor(c)
end
end
function timerPrototype:DisableEnlarge(...)
local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
local barGroup = self.mod.barGroup or DBM.Bars;
local bar = barGroup:GetBar(id)
if bar then
bar.small = true
end
end
function timerPrototype:AddOption(optionDefault, optionName)
if optionName ~= false then
self.option = optionName or self.id
self.mod:AddBoolOption(self.option, optionDefault, "timer")
end
end
function bossModPrototype:NewTimer(timer, name, icon, optionDefault, optionName, r, g, b)
local icon = type(icon) == "number" and select(3, GetSpellInfo(icon)) or icon
local obj = setmetatable(
{
text = self.localization.timers[name],
timer = timer,
id = name,
icon = icon,
r = r,
g = g,
b = b,
startedTimers = {},
mod = self,
},
mt
)
obj:AddOption(optionDefault, optionName)
table.insert(self.timers, obj)
return obj
end
-- new constructor for the new auto-localized timer types
-- note that the function might look unclear because it needs to handle different timer types, especially achievement timers need special treatment
-- todo: disable the timer if the player already has the achievement and when the ACHIEVEMENT_EARNED event is fired
-- problem: heroic/normal achievements :[
-- local achievementTimers = {}
local function newTimer(self, timerType, timer, spellId, timerText, optionDefault, optionName, texture, r, g, b)
-- new argument timerText is optional (usually only required for achievement timers as they have looooong names)
if type(timerText) == "boolean" or type(optionDefault) == "string" then -- check if the argument was skipped
return newTimer(self, timerType, timer, spellId, nil, timerText, optionDefault, optionName, texture, r, g, b)
end
local spellName, icon
if timerType == "achievement" then
spellName = select(2, GetAchievementInfo(spellId))
icon = type(texture) == "number" and select(10, GetAchievementInfo(texture)) or texture or spellId and select(10, GetAchievementInfo(spellId))
-- if optionDefault == nil then
-- local completed = select(4, GetAchievementInfo(spellId))
-- optionDefault = not completed
-- end
else
spellName = GetSpellInfo(spellId or 0)
if spellName then
icon = type(texture) == "number" and select(3, GetSpellInfo(texture)) or texture or spellId and select(3, GetSpellInfo(spellId))
else
icon = nil
end
end
spellName = spellName or tostring(spellId)
local id = "Timer"..(spellId or 0)..self.id..#self.timers
local obj = setmetatable(
{
text = self.localization.timers[timerText],
type = timerType,
spellId = spellId,
timer = timer,
id = id,
icon = icon,
r = r,
g = g,
b = b,
startedTimers = {},
mod = self,
},
mt
)
obj:AddOption(optionDefault, optionName)
table.insert(self.timers, obj)
-- todo: move the string creation to the GUI with SetFormattedString...
if timerType == "achievement" then
self.localization.options[id] = DBM_CORE_AUTO_TIMER_OPTIONS[timerType]:format(GetAchievementLink(spellId):gsub("%[(.+)%]", "%1"))
elseif timerType == "phase" then
self.localization.options[id] = DBM_CORE_AUTO_TIMER_OPTIONS[timerType]:format(spellId, timerText)
else
self.localization.options[id] = DBM_CORE_AUTO_TIMER_OPTIONS[timerType]:format(spellId, spellName)
end
return obj
end
function bossModPrototype:NewTargetTimer(...)
return newTimer(self, "target", ...)
end
function bossModPrototype:NewBuffActiveTimer(...)
return newTimer(self, "active", ...)
end
function bossModPrototype:NewCastTimer(timer, ...)
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 ;)
local spellId = timer
timer = select(7, GetSpellInfo(spellId)) or 1000 -- GetSpellInfo takes YOUR spell haste into account...WTF?
local spellHaste = select(7, GetSpellInfo(53142)) / 10000 -- 53142 = Dalaran Portal, should have 10000 ms cast time
timer = timer / spellHaste -- calculate the real cast time of the spell...
return self:NewCastTimer(timer / 1000, spellId, ...)
end
return newTimer(self, "cast", timer, ...)
end
function bossModPrototype:NewCDTimer(...)
return newTimer(self, "cd", ...)
end
function bossModPrototype:NewNextTimer(...)
return newTimer(self, "next", ...)
end
function bossModPrototype:NewAchievementTimer(...)
return newTimer(self, "achievement", ...)
end
function bossModPrototype:NewPhaseTimer(...)
return newTimer(self, "phase", ...)
end
function bossModPrototype:GetLocalizedTimerText(timerType, spellId)
local spellName
if timerType == "achievement" then
spellName = select(2, GetAchievementInfo(spellId))
else
spellName = GetSpellInfo(spellId)
end
return pformat(DBM_CORE_AUTO_TIMER_TEXTS[timerType], spellName)
end
end
---------------------
-- Enrage Object --
---------------------
do
local enragePrototype = {}
local mt = {__index = enragePrototype}
function enragePrototype:Start(timer)
timer = timer or self.timer or 600
timer = timer <= 0 and self.timer - timer or timer
self.bar:SetTimer(timer)
self.bar:Start()
if timer > 660 then self.warning1:Schedule(timer - 600, 10, DBM_CORE_MIN) end
if timer > 300 then self.warning1:Schedule(timer - 300, 5, DBM_CORE_MIN) end
if timer > 180 then self.warning2:Schedule(timer - 180, 3, DBM_CORE_MIN) end
if timer > 60 then self.warning2:Schedule(timer - 60, 1, DBM_CORE_MIN) end
if timer > 30 then self.warning2:Schedule(timer - 30, 30, DBM_CORE_SEC) end
if timer > 10 then self.warning2:Schedule(timer - 10, 10, DBM_CORE_SEC) end
end
function enragePrototype:Schedule(t)
return self.owner:Schedule(t, self.Start, self)
end
function enragePrototype:Cancel()
self.owner:Unschedule(self.Start, self)
self.warning1:Cancel()
self.warning2:Cancel()
self.bar:Stop()
end
enragePrototype.Stop = enragePrototype.Cancel
function bossModPrototype:NewBerserkTimer(timer, text, barText, barIcon, spellID)
if type(text) == "number" then
spellID = text;
text = nil;
end
spellID = spellID or 26662;
local spellName,_,spellIcon = GetSpellInfo(spellID);
timer = timer or 600
local warning1 = self:NewAnnounce(text or DBM_CORE_GENERIC_WARNING_BERSERK, 1, nil, "warning_berserk", false)
local warning2 = self:NewAnnounce(text or DBM_CORE_GENERIC_WARNING_BERSERK, 4, nil, "warning_berserk", false)
local bar = self:NewTimer(timer or 600, barText or DBM_CORE_GENERIC_TIMER_BERSERK, barIcon or spellIcon or spellID, nil, "timer_berserk")
local obj = setmetatable(
{
warning1 = warning1,
warning2 = warning2,
bar = bar,
timer = timer,
owner = self
},
mt
)
self.localization.options.timer_berserk = DBM_CORE_OPTION_TIMER_BERSERK_CUSTOM:format(spellID, spellName or "Berserk");
return obj
end
end
---------------
-- Options --
---------------
function bossModPrototype:AddBoolOption(name, default, cat, func)
cat = cat or "misc"
self.Options[name] = (default == nil) or default
self:SetOptionCategory(name, cat)
if func then
self.optionFuncs = self.optionFuncs or {}
self.optionFuncs[name] = func
end
end
function bossModPrototype:RemoveOption(name)
self.Options[name] = nil
for i, options in pairs(self.optionCategories) do
removeEntry(options, name)
if #options == 0 then
self.optionCategories[i] = nil
removeEntry(self.categorySort, i)
end
end
if self.optionFuncs then
self.optionFuncs[name] = nil
end
end
function bossModPrototype:AddSliderOption(name, minValue, maxValue, valueStep, default, cat, func)
cat = cat or "misc"
self.Options[name] = default or 0
self:SetOptionCategory(name, cat)
self.sliders = self.sliders or {}
self.sliders[name] = {
minValue = minValue,
maxValue = maxValue,
valueStep = valueStep,
}
if func then
self.optionFuncs = self.optionFuncs or {}
self.optionFuncs[name] = func
end
end
function bossModPrototype:AddButton(name, onClick, cat, func)
cat = cat or misc
self:SetOptionCategory(name, cat)
self.buttons = self.buttons or {}
self.buttons[name] = onClick
if func then
self.optionFuncs = self.optionFuncs or {}
self.optionFuncs[name] = func
end
end
function bossModPrototype:AddDropdownOption(name, options, default, cat, func)
cat = cat or "misc"
self.Options[name] = default
self:SetOptionCategory(name, cat)
self.dropdowns = self.dropdowns or {}
self.dropdowns[name] = options
if func then
self.optionFuncs = self.optionFuncs or {}
self.optionFuncs[name] = func
end
end
function bossModPrototype:AddOptionSpacer(cat)
cat = cat or "misc"
if self.optionCategories[cat] then
table.insert(self.optionCategories[cat], DBM_OPTION_SPACER)
end
end
function bossModPrototype:AddAnnounceSpacer()
return self:AddOptionSpacer("announce")
end
function bossModPrototype:AddTimerSpacer()
return self:AddOptionSpacer("timer")
end
function bossModPrototype:SetOptionCategory(name, cat)
for _, options in pairs(self.optionCategories) do
removeEntry(options, name)
end
if not self.optionCategories[cat] then
self.optionCategories[cat] = {}
table.insert(self.categorySort, cat)
end
table.insert(self.optionCategories[cat], name)
end
--------------
-- Combat --
--------------
function bossModPrototype:RegisterCombat(cType, ...)
if cType then
cType = cType:lower()
end
local info = {
type = cType,
mob = self.creatureId,
name = self.localization.general.name or self.id,
msgs = (cType ~= "combat") and {...},
mod = self
}
if self.multiMobPullDetection then
info.multiMobPullDetection = self.multiMobPullDetection
end
local addedKillMobs = false
for i = 1, select("#", ...) do
local v = select(i, ...)
if type(v) == "number" then
info.killMobs = info.killMobs or {}
info.killMobs[select(i, ...)] = true
addedKillMobs = true
end
end
if not addedKillMobs and self.multiMobPullDetection then
for i, v in ipairs(self.multiMobPullDetection) do
info.killMobs = info.killMobs or {}
info.killMobs[v] = true
end
end
self.combatInfo = info
if not self.zones then return end
for i, v in ipairs(self.zones) do
combatInfo[v] = combatInfo[v] or {}
table.insert(combatInfo[v], info)
end
end
-- needs to be called _AFTER_ RegisterCombat
function bossModPrototype:RegisterKill(msgType, ...)
if cType then
cType = cType:lower()
end
if not self.combatInfo then
return
end
self.combatInfo.killType = msgType
self.combatInfo.killMsgs = {}
for i = 1, select("#", ...) do
local v = select(i, ...)
self.combatInfo.killMsgs[v] = true
end
end
-- needs to be called _AFTER_ RegisterCombat
function bossModPrototype:SetDetectCombatInVehicle(flag)
if not self.combatInfo then
return
end
self.combatInfo.noCombatInVehicle = not flag
end
function bossModPrototype:IsInCombat()
return self.inCombat
end
function bossModPrototype:SetMinCombatTime(t)
self.minCombatTime = t
end
-- needs to be called after RegisterCombat
function bossModPrototype:SetWipeTime(t)
self.combatInfo.wipeTimer = t
end
function bossModPrototype:GetBossHPString(cId)
local idType = (GetNumRaidMembers() == 0 and "party") or "raid"
for i = 0, math.max(GetNumRaidMembers(), GetNumPartyMembers()) do
local unitId = ((i == 0) and "target") or idType..i.."target"
local guid = UnitGUID(unitId)
if guid and tonumber(guid:sub(9, 12), 16) == cId then
return math.floor(UnitHealth(unitId)/UnitHealthMax(unitId) * 100).."%"
end
end
return DBM_CORE_UNKNOWN
end
function bossModPrototype:GetHP()
return self:GetBossHPString((self.combatInfo and self.combatInfo.mob) or self.creatureId)
end
function bossModPrototype:IsWipe()
local wipe = true
local uId = ((GetNumRaidMembers() == 0) and "party") or "raid"
for i = 0, math.max(GetNumRaidMembers(), GetNumPartyMembers()) do
local id = (i == 0 and "player") or uId..i
if UnitAffectingCombat(id) and not UnitIsDeadOrGhost(id) then
wipe = false
break
end
end
return wipe
end
-----------------------
-- Synchronization --
-----------------------
function bossModPrototype:SendSync(event, arg)
event = event or ""
arg = arg or ""
local str = ("%s\t%s\t%s\t%s"):format(self.id, self.revision or 0, event, arg)
local spamId = self.id..event..arg
local time = GetTime()
if not modSyncSpam[spamId] or (time - modSyncSpam[spamId]) > 2.5 then
self:ReceiveSync(event, arg, nil, self.revision or 0)
sendSync("DBMv4-Mod", str)
end
end
function bossModPrototype:ReceiveSync(event, arg, sender, revision)
local spamId = self.id..event..arg
local time = GetTime()
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
modSyncSpam[spamId] = time
self:OnSync(event, arg, sender)
end
end
function bossModPrototype:SetMinSyncRevision(revision)
self.minSyncRevision = revision
end
-----------------
-- Scheduler --
-----------------
function bossModPrototype:Schedule(t, f, ...)
return schedule(t, f, self, ...)
end
function bossModPrototype:Unschedule(f, ...)
return unschedule(f, self, ...)
end
function bossModPrototype:ScheduleMethod(t, method, ...)
if not self[method] then
error(("Method %s does not exist"):format(tostring(method)), 2)
end
return self:Schedule(t, self[method], self, ...)
end
bossModPrototype.ScheduleEvent = bossModPrototype.ScheduleMethod
function bossModPrototype:UnscheduleMethod(method, ...)
if not self[method] then
error(("Method %s does not exist"):format(tostring(method)), 2)
end
return self:Unschedule(self[method], self, ...)
end
bossModPrototype.UnscheduleEvent = bossModPrototype.UnscheduleMethod
-------------
-- Icons --
-------------
function bossModPrototype:SetIcon(target, icon, timer)
if DBM.Options.DontSetIcons or not enableIcons or DBM:GetRaidRank() == 0 then
return
end
icon = icon and icon >= 0 and icon <= 8 and icon or 8
local oldIcon = self:GetIcon(target) or 0
SetRaidTarget(DBM:GetRaidUnitId(target), icon)
self:UnscheduleMethod("SetIcon", target)
if timer then
self:ScheduleMethod(timer, "RemoveIcon", target)
if oldIcon then
self:ScheduleMethod(timer + 1, "SetIcon", target, oldIcon)
end
end
end
function bossModPrototype:GetIcon(target)
return GetRaidTargetIndex(DBM:GetRaidUnitId(target))
end
function bossModPrototype:RemoveIcon(target, timer)
return self:SetIcon(target, 0, timer)
end
function bossModPrototype:ClearIcons()
if GetNumRaidMembers() > 0 then
for i = 1, GetNumRaidMembers() do
if UnitExists("raid"..i) and GetRaidTargetIndex("raid"..i) then
SetRaidTarget("raid"..i, 0)
end
end
else
for i = 1, GetNumPartyMembers() do
if UnitExists("party"..i) and GetRaidTargetIndex("party"..i) then
SetRaidTarget("party"..i, 0)
end
end
end
end
-----------------------
-- Model Functions --
-----------------------
function bossModPrototype:SetModelScale(scale)
self.modelScale = scale
end
function bossModPrototype:SetModelOffset(x, y, z)
self.modelOffsetX = x
self.modelOffsetY = y
self.modelOffsetZ = z
end
function bossModPrototype:SetModelRotation(r)
self.modelRotation = r
end
function bossModPrototype:SetModelMoveSpeed(v)
self.modelMoveSpeed = v
end
function bossModPrototype:SetModelID(id)
self.modelId = id
end
function bossModPrototype:EnableModel()
self.modelEnabled = true
end
function bossModPrototype:DisableModel()
self.modelEnabled = nil
end
function bossModPrototype:GetHealth(unit)
local hp = UnitHealth(unit) / UnitHealthMax(unit) * 100
return hp
end
--------------------
-- Localization --
--------------------
function bossModPrototype:GetLocalizedStrings()
return self.localization.miscStrings
end
-- Not really good, needs a few updates
do
local modLocalizations = {}
local modLocalizationPrototype = {}
local mt = {__index = modLocalizationPrototype}
local returnKey = {__index = function(t, k) return k end}
local defaultCatLocalization = {
__index = setmetatable({
timer = DBM_CORE_OPTION_CATEGORY_TIMERS,
announce = DBM_CORE_OPTION_CATEGORY_WARNINGS,
misc = DBM_CORE_OPTION_CATEGORY_MISC
}, returnKey)
}
local defaultTimerLocalization = {
__index = setmetatable({
timer_berserk = DBM_CORE_GENERIC_TIMER_BERSERK,
TimerSpeedKill = DBM_CORE_ACHIEVEMENT_TIMER_SPEED_KILL
}, returnKey)
}
local defaultAnnounceLocalization = {
__index = setmetatable({
warning_berserk = DBM_CORE_GENERIC_WARNING_BERSERK
}, returnKey)
}
local defaultOptionLocalization = {
__index = setmetatable({
timer_berserk = DBM_CORE_OPTION_TIMER_BERSERK,
HealthFrame = DBM_CORE_OPTION_HEALTH_FRAME
}, returnKey)
}
local defaultMiscLocalization = {
__index = function(t, k)
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
end
}
function modLocalizationPrototype:SetGeneralLocalization(t)
for i, v in pairs(t) do
self.general[i] = v
end
end
function modLocalizationPrototype:SetWarningLocalization(t)
for i, v in pairs(t) do
self.warnings[i] = v
end
end
function modLocalizationPrototype:SetTimerLocalization(t)
for i, v in pairs(t) do
self.timers[i] = v
end
end
function modLocalizationPrototype:SetOptionLocalization(t)
for i, v in pairs(t) do
self.options[i] = v
end
end
function modLocalizationPrototype:SetOptionCatLocalization(t)
for i, v in pairs(t) do
self.cats[i] = v
end
end
function modLocalizationPrototype:SetMiscLocalization(t)
for i, v in pairs(t) do
self.miscStrings[i] = v
end
end
function DBM:CreateModLocalization(name)
local obj = {
general = setmetatable({}, returnKey),
warnings = setmetatable({}, defaultAnnounceLocalization),
options = setmetatable({}, defaultOptionLocalization),
timers = setmetatable({}, defaultTimerLocalization),
miscStrings = setmetatable({}, defaultMiscLocalization),
cats = setmetatable({}, defaultCatLocalization),
}
obj.miscStrings.misc = obj
setmetatable(obj, mt)
modLocalizations[name] = obj
return obj
end
function DBM:GetModLocalization(name)
return modLocalizations[name] or self:CreateModLocalization(name)
end
end