local myname, ns = ... local core = LibStub("AceAddon-3.0"):GetAddon("SilverDragon") local module = core:NewModule("Announce", "AceTimer-3.0", "LibSink-2.0") local Debug = core.Debug local LSM = LibStub("LibSharedMedia-3.0") -- Register some media LSM:Register("sound", "Rubber Ducky", 566121) LSM:Register("sound", "Cartoon FX", 566543) LSM:Register("sound", "Explosion", 566982) LSM:Register("sound", "Shing!", 566240) LSM:Register("sound", "Wham!", 566946) LSM:Register("sound", "Simon Chime", 566076) LSM:Register("sound", "War Drums", 567275)--NPC Scan default LSM:Register("sound", "Scourge Horn", 567386)--NPC Scan default LSM:Register("sound", "Pygmy Drums", 566508) LSM:Register("sound", "Cheer", 567283) LSM:Register("sound", "Humm", 569518) LSM:Register("sound", "Short Circuit", 568975) LSM:Register("sound", "Fel Portal", 569215) LSM:Register("sound", "Fel Nova", 568582) LSM:Register("sound", "PVP Flag", 569200) LSM:Register("sound", "Thunder crack", 566202) -- doodad/fx_thundercrack04.ogg LSM:Register("sound", "Algalon: Beware!", 543587) LSM:Register("sound", "Yogg Saron: Laugh", 564859) LSM:Register("sound", "Illidan: Not Prepared", 552503) LSM:Register("sound", "Magtheridon: I am Unleashed", 554554) LSM:Register("sound", "Loatheb: I see you", 554236) LSM:Register("sound", "Ikiss: Trinkets", 561403) LSM:Register("sound", "NPCScan", 567275)--Sound file is actually bogus, this just forces the option NPCScan into menu. We hack it later. function module:OnInitialize() self.db = core.db:RegisterNamespace("Announce", { profile = { sink = true, drums = true, sound = true, soundgroup = true, soundguild = false, sound_mount = true, sound_boss = true, sound_loot = true, soundfile = "Loatheb: I see you", soundfile_mount = "Illidan: Not Prepared", soundfile_boss = "Magtheridon: I am Unleashed", soundfile_loot = "Ikiss: Trinkets", sound_loop = 1, sound_mount_loop = 1, sound_boss_loop = 1, sound_loot_loop = 1, flash = true, flash_texture = "Blizzard Low Health", flash_color = {r=1,g=0,b=1,a=1,}, flash_mount = true, flash_texture_mount = "Blizzard Low Health", flash_color_mount = {r=0,g=1,b=0,a=1,}, flash_boss = false, flash_texture_boss = "Blizzard Low Health", flash_color_boss = {r=1,g=0,b=1,a=1,}, instances = false, dead = true, already = false, already_drop = true, already_transmog = false, already_alt = true, sink_opts = {}, channel = "Master", unmute = false, background = false, loot = true, }, }) self:SetSinkStorage(self.db.profile.sink_opts) if self.db.profile.sink_opts.sink20OutputSink == "Channel" then -- 8.2.5 / Classic removed the ability to output to channels, outside of hardware-driven events self.db.profile.sink_opts.sink20OutputSink = "Default" end core.RegisterCallback(self, "Seen") core.RegisterCallback(self, "SeenLoot") local config = core:GetModule("Config", true) if config then local toggle = config.toggle local get = function(info) return self.db.profile[info[#info]] end local set = function(info, v) self.db.profile[info[#info]] = v end local sink_config = self:GetSinkAce3OptionsDataTable() local sink_args = {} for k,v in pairs(sink_config.args) do if k ~= "Channel" then sink_args[k] = v end end sink_config.args = sink_args sink_config.inline = true sink_config.order = 15 sink_config.args.Channel = nil local faker = function(id, name, zone, x, y) return { type = "execute", name = name, desc = "Fake seeing " .. name, func = function() -- id, zone, x, y, is_dead, source, unit core.events:Fire("Seen", id, zone, x, y, false, "fake", false) end, } end local soundfile = function(enabled_key, order) return { type = "select", dialogControl = "LSM30_Sound", name = "Sound to Play", desc = "Choose a sound file to play", values = AceGUIWidgetLSMlists.sound, disabled = function() return not self.db.profile[enabled_key] end, order = order, } end local soundrange = function(order) return { type = "range", name = "Repeat...", desc = "How many times to repeat the sound", min = 1, max = 10, step = 1, order = order, } end local colorget = function(info) local color = self.db.profile[info[#info]] return color.r, color.g, color.b, color.a end local colorset = function(info, r, g, b, a) local color = self.db.profile[info[#info]] color.r, color.g, color.b, color.a = r, g, b, a end local options = { general = { type = "group", name = "General", inline = true, order = 10, get = get, set = set, args = { already = toggle("Already found", "Announce when we see rares we've already killed / achieved (if known)", 0), already_drop = toggle("Got the loot", "Announce when we see rares which drop a mount / toy / pet you already have", 10), already_transmog = toggle("...include transmog as loot", "Count transmog appearances as knowable loot for the previous option?", 11), already_alt = toggle("Completed by an alt", "Announce when we see rares for an achievement that the current character doesn't have, but an alt has completed already", 20), dead = toggle("Dead rares", "Announce when we see dead rares, if known. Not all scanning methods know whether a rare is dead or not, so this isn't entirely reliable.", 30), instances = toggle("Instances", "Show announcements while in an instance", 50), loot = toggle("Treasures", "Show announcements when treasure appears on the minimap", 60), }, }, message = { type = "group", name = "Messages", order = 20, get = get, set = set, args = { sink = toggle("Enabled", "Send a message to whatever scrolling text addon you're using.", 10), output = sink_config, }, }, test = { type = "group", name = "Test it!", inline = true, args = { -- id, name, zone, x, y, is_dead, is_new_location, source, unit time = faker(32491, "Time-Lost Proto Drake (Mount!)", 120, 0.490, 0.362), -- ishak = faker(157134, "Ishak of the Four Winds (Mount!)", 1527, 0.73, 0.83), anger = faker(60491, "Sha of Anger (Boss!)", 809, 0.5, 0.5), vyragosa = faker(32630, "Vyragosa (Boring)", 120, 0.5, 0.5), deathmaw = faker(10077, "Deathmaw (Tame!)", 29, 0.5, 0.5), -- haakun = faker(83008, "Haakun", 946, 0.5, 0.5), yiphrim = faker(157473, "Yiphrim the Will Ravager (Toy!)", 1527, 0.5, 0.786), amalgamation = faker(157593, "Amalgamation of Flesh (Pet!)", 1527, 0.598, 0.724), -- alash = faker(148787, "Alash'anir", 62, 0.598, 0.724), -- burninator = faker(149141, "Burninator Mk V (Pet!)", 62, 0.414, 0.764), worldedge = faker(160821, "Worldedge Gorger (mount)", 1525, 0.5, 0.5), tarahna = faker(126900, "Instructor Tarahna (multi-toy)", 882, 0.5, 0.5), nerissa = faker(162690, "Nerissa Heartless (mount)", 1536, 0.5, 0.5), -- faeflayer = faker(171688, "Faeflayer", 1536, 0.5, 0.5), scrapking = faker(151625, "Scrap King (loot)", 1462, 0.5, 0.5), kash = faker(159105, "Collector Kash (lots of loot)", 1536, 0.5, 0.5), chest = { type = "execute", name = "Waterlogged Chest", desc = "Fake seeing a Waterlogged Chest", func = function() -- id, zone, x, y, is_dead, source, unit core.events:Fire("SeenLoot", "Waterlogged Chest", 3341, 37, 0.318, 0.628) end, }, }, }, sound = { type = "group", name = "Sounds", get = get, set = set, order = 10, args = { about = config.desc("Play sounds to announce rare mobs? Can do special things for special mobs. You *really* don't want to miss, say, the Time-Lost Proto Drake, after all...", 0), channel = { type = "select", name = _G.SOUND_CHANNELS, descStyle = "inline", values = { Ambience = _G.AMBIENCE_VOLUME, Master = _G.MASTER, Music = _G.MUSIC_VOLUME, SFX = _G.SOUND_VOLUME, Dialog = _G.DIALOG_VOLUME, }, order = 11, }, unmute = toggle("Ignore mute", "Play sounds even when muted", 12), background = toggle(_G.ENABLE_BGSOUND, _G.OPTION_TOOLTIP_ENABLE_BGSOUND, 13), drums = toggle("The Sound of Drums", "Underneath it all, the constant drumming", 14), soundgroup = toggle("Group Sync Sounds", "Play sounds from synced mobs from party/raid members", 15), soundguild = toggle("Guild Sync Sounds", "Play sounds from synced mobs from guild members not in group", 16), regular = {type="header", name="", order=20,}, sound = toggle("Sounds", "Play sounds for regular mobs", 21), soundfile = soundfile("sound", 22), sound_loop = soundrange(23), mount = {type="header", name="", order=25,}, sound_mount = toggle("Mount sounds", "Play a sound for mobs that drop a mount", 26), soundfile_mount = soundfile("sound_mount", 27), sound_mount_loop = soundrange(28), boss = {type="header", name="", order=30,}, sound_boss = toggle("Boss sounds", "Play a sound for mobs that require a group", 31), soundfile_boss = soundfile("sound_boss", 35), sound_boss_loop = soundrange(37), loot = {type="header", name="", order=40,}, sound_loot = toggle("Loot sounds", "Play a sound for treasures", 41), soundfile_loot = soundfile("sound_loot", 45), sound_loot_loop = soundrange(47), }, }, flash = { type = "group", name = "Flash", get = get, set = set, order = 15, args = { about = config.desc("Flash the screen when a rare is seen.", 0), flash = toggle("Enabled", "Flash the screen?", 1), flash_color = { name = COLOR, type = "color", hasAlpha = true, descStyle = "inline", get = colorget, set = colorset, order = 2, }, flash_texture = { name = TEXTURES_SUBHEADER, type = "select", descStyle = "inline", dialogControl = "LSM30_Background", values = AceGUIWidgetLSMlists.background, order = 3, }, preview = { name = PREVIEW, type = "execute", func = function() module:Flash(50065) -- Armagedillo end, order = 4, }, mount = {type="header", name="", order=10,}, flash_mount = toggle("Mount flash", "Flash the screen differently when we see a mob with a mount?", 11), flash_color_mount = { name = COLOR, type = "color", hasAlpha = true, descStyle = "inline", get = colorget, set = colorset, order = 12, }, flash_texture_mount = { name = TEXTURES_SUBHEADER, type = "select", descStyle = "inline", dialogControl = "LSM30_Background", values = AceGUIWidgetLSMlists.background, order = 13, }, preview_mount = { name = PREVIEW, type = "execute", func = function() module:Flash(32491) -- time lost end, order = 14, }, boss = {type="header", name="", order=20,}, flash_boss = toggle("Boss flash", "Flash the screen differently when we see a boss rare?", 21), flash_color_boss = { name = COLOR, type = "color", hasAlpha = true, descStyle = "inline", get = colorget, set = colorset, order = 22, }, flash_texture_boss = { name = TEXTURES_SUBHEADER, type = "select", descStyle = "inline", dialogControl = "LSM30_Background", values = AceGUIWidgetLSMlists.background, order = 23, }, preview_boss = { name = PREVIEW, type = "execute", func = function() module:Flash(70096) -- War-God Dokah end, order = 24, }, }, } } config.options.args.outputs.plugins.announce = options end end function module:Seen(callback, id, zone, x, y, is_dead, source, ...) Debug("Announce:Seen", id, zone, x, y, is_dead, ...) if not self.db.profile.instances and IsInInstance() then return end if not self:ShouldAnnounce(id, zone, x, y, is_dead, source, ...) then return end core.events:Fire("Announce", id, zone, x, y, is_dead, source, ...) end function module:SeenLoot(callback, name, id, zone, x, y, ...) Debug("Announce:SeenLoot", name, id, zone, x, y, ...) if not self.db.profile.instances and IsInInstance() then return end if not self.db.profile.loot then return end core.events:Fire("AnnounceLoot", name, id, zone, x, y, ...) end function module:ShouldAnnounce(id, zone, x, y, is_dead, source, ...) if is_dead and not self.db.profile.dead then Debug("ShouldAnnounce", false, "dead") return false end if core.db.global.always[id] then -- If you've manually added a mob, bypass any other checks Debug("ShouldAnnounce", true, "always") return true end if not self.db.profile.already_drop and ns.Loot.Status(id, self.db.profile.already_transmog) == true then -- hide mobs which have a mount/pet/toy which you already own -- this means there's knowable loot, and it's all known Debug("ShouldAnnounce", false, "already got loot") return false end if not self.db.profile.already then -- hide already-completed mobs local quest, achievement, by_alt = ns:CompletionStatus(id) if by_alt and not self.db.profile.already_alt then -- an alt has completed the achievement, and we don't want to know about that Debug("ShouldAnnounce", false, "alt got achievement") return false end if source == "vignette" then -- The vignette's presence implies no quest completion Debug("ShouldAnnounce", true, "vignette implies quest") return true end if quest ~= nil then Debug("ShouldAnnounce", not quest, "quest") return not quest end if achievement ~= nil then -- can just fall back on achievement Debug("ShouldAnnounce", not achievement, "achievement") return not achievement end end Debug("ShouldAnnounce", true, "fallback") return true end core.RegisterCallback("SD Announce Sink", "Announce", function(callback, id, zone, x, y, dead, source) if not module.db.profile.sink then return end Debug("Pouring") if source:match("^sync") then local channel, player = source:match("sync:(.+):(.+)") if channel and player then local localized_zone = core.zone_names[zone] or UNKNOWN source = "by " .. player .. " in your " .. strlower(channel) .. "; " .. localized_zone end end if x and y then if x == 0 and y == 0 then source = source .. " @ unknown location" else source = source .. " @ " .. core.round(x * 100, 1) .. "," .. core.round(y * 100, 1) end end local prefix = "Rare seen: " module:Pour((prefix .. "%s%s (%s)"):format(core:GetMobLabel(id), dead and "... but it's dead" or '', source or '')) end) core.RegisterCallback("SD AnnounceLoot Sink", "AnnounceLoot", function(callback, name, id, zone, x, y) if not module.db.profile.sink then return end Debug("Pouring") local location = UNKNOWN if x and y and x > 0 and y > 0 then location = core.round(x * 100, 1) .. "," .. core.round(y * 100, 1) end local prefix = "Treasure seen: " module:Pour((prefix .. "%s (%s)"):format(name, location)) end) local cvar_overrides local channel_cvars = { Ambience = "Sound_EnableAmbience", Master = "Sound_EnableAllSound", Music = "Sound_EnableMusic", SFX = "Sound_EnableSFX", Dialog = "Sound_EnableDialog", } local delays = { ["Ikiss: Trinkets"] = 5.7, } local nowplaying function module:PlaySound(s) -- Arg is a table, to make scheduling the loops easier. I am lazy. Debug("Playing sound", s.soundfile, s.loops) -- boring check: if s and s.handle then StopSound(s.handle) if s.drumshandle then StopSound(s.drumshandle) end s.handle = nil s.drumshandle = nil end if not s.loops or s.loops == 0 then if cvar_overrides and s.cvars then for cvar, value in pairs(s.cvars) do SetCVar(cvar, value) end cvar_overrides = false end nowplaying = false return end if not cvar_overrides then if self.db.profile.background and GetCVar("Sound_EnableSoundWhenGameIsInBG") == "0" then cvar_overrides = true s.cvars = s.cvars or {} s.cvars["Sound_EnableSoundWhenGameIsInBG"] = GetCVar("Sound_EnableSoundWhenGameIsInBG") SetCVar("Sound_EnableSoundWhenGameIsInBG", "1") end if self.db.profile.unmute and GetCVar(channel_cvars[self.db.profile.channel]) == "0" then cvar_overrides = true s.cvars = s.cvars or {} s.cvars[channel_cvars[self.db.profile.channel]] = GetCVar(channel_cvars[self.db.profile.channel]) SetCVar(channel_cvars[self.db.profile.channel], "1") end end -- now, noise! local drums = self.db.profile.drums if s.soundfile == "NPCScan" then --Override default behavior and force npcscan behavior of two sounds at once drums = true local _, handle = PlaySoundFile(LSM:Fetch("sound", "Scourge Horn"), self.db.profile.channel) s.handle = handle else --Play whatever sound is set local _, handle = PlaySoundFile(LSM:Fetch("sound", s.soundfile), self.db.profile.channel) s.handle = handle end if drums then local _, handle = PlaySoundFile(LSM:Fetch("sound", "War Drums"), self.db.profile.channel) s.drumshandle = handle end s.loops = s.loops - 1 -- we guarantee one callback, in case we need to do cleanup self:ScheduleTimer("PlaySound", delays[s.soundfile] or 4.5, s) nowplaying = true end core.RegisterCallback("SD Announce Sound", "Announce", function(callback, id, zone, x, y, dead, source) if not LSM then return end if nowplaying then return end if source:match("^sync") then local channel, player = source:match("sync:(.+):(.+)") if channel == "GUILD" and not module.db.profile.soundguild or (channel == "PARTY" or channel == "RAID") and not module.db.profile.soundgroup then return end end local soundfile, loops if ns.Loot.HasMounts(id) then if not module.db.profile.sound_mount then return end soundfile = module.db.profile.soundfile_mount loops = module.db.profile.sound_mount_loop elseif ns.mobdb[id] and ns.mobdb[id].boss then if not module.db.profile.sound_boss then return end soundfile = module.db.profile.soundfile_boss loops = module.db.profile.sound_boss_loop else if not module.db.profile.sound then return end soundfile = module.db.profile.soundfile loops = module.db.profile.sound_loop end module:PlaySound{soundfile = soundfile, loops = loops} end) core.RegisterCallback("SD AnnounceLoot Sound", "AnnounceLoot", function(callback, id, zone, x, y, dead, source) if not (module.db.profile.sound_loot and LSM) then return end if nowplaying then return end module:PlaySound{soundfile = module.db.profile.soundfile_loot, loops = module.db.profile.sound_loot_loop} end) do local flashframe function module:Flash(id) if not module.db.profile.flash then return end if not flashframe then flashframe = CreateFrame("Frame", nil, WorldFrame) flashframe:SetClampedToScreen(true) flashframe:SetFrameStrata("FULLSCREEN_DIALOG") flashframe:SetToplevel(true) flashframe:SetAllPoints(UIParent) flashframe:Hide() -- Use the OutOfControl (blue) and LowHealth (red) textures to get a purple flash local texture = flashframe:CreateTexture(nil, "BACKGROUND") texture:SetBlendMode("ADD") texture:SetDesaturated(true) texture:SetAllPoints() local group = flashframe:CreateAnimationGroup() group:SetLooping("BOUNCE") local pulse = group:CreateAnimation("Alpha") pulse:SetFromAlpha(0.3) pulse:SetToAlpha(0.75) pulse:SetDuration(0.5236) local loops = 0 group:SetScript("OnLoop", function(frame, state) loops = loops + 1 if loops == 9 then group:Finish() end end) group:SetScript("OnFinished", function(self) loops = 0 flashframe:Hide() end) flashframe:SetScript("OnShow", function(self) local background = module.db.profile.flash_texture local color = module.db.profile.flash_color if self.id and ns.mobdb[self.id] then if ns.Loot.HasMounts(self.id) and module.db.profile.flash_mount then background = module.db.profile.flash_texture_mount color = module.db.profile.flash_color_mount elseif ns.mobdb[self.id].boss and module.db.profile.flash_boss then background = module.db.profile.flash_texture_boss color = module.db.profile.flash_color_boss end end texture:SetTexture(LSM:Fetch("background", background)) texture:SetVertexColor(color.r, color.g, color.b, color.a) group:Play() end) end Debug("Flashing") flashframe.id = id flashframe:Hide() flashframe:Show() end core.RegisterCallback("SD Announce Flash", "Announce", function(callback, id) module:Flash(id) end) core.RegisterCallback("SD AnnounceLoot Flash", "AnnounceLoot", function(callback, id) module:Flash(id) end) end