82a9ac0937
* 5.20 * Update to 5.21 --------- Co-authored-by: Szyler <Szyler@Szyler.com>
4051 lines
128 KiB
Lua
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
|
|
|