local version = '1.0' local author = 'shirsig' local BROWN = { .7, .15, .05 } local YELLOW = { 1, 1, .15 } local CHECK_INTERVAL = .5 npcscan_targets = {} npcscan = {} function npcscan.without_errors(f) local orig = UIErrorsFrame.AddMessage UIErrorsFrame.AddMessage = Aux.util.pass f() UIErrorsFrame.AddMessage = orig end do local last_played function npcscan.play_sound() if not last_played or GetTime() - last_played > 10 then -- 8 PlaySoundFile('Interface\\AddOns\\npcscan\\Event_wardrum_ogre.ogg', 'Master') PlaySoundFile('Interface\\AddOns\\npcscan\\scourge_horn.ogg', 'Master') last_played = GetTime() end end end function npcscan.check_for_targets() for name, _ in pairs(npcscan_targets) do -- local original_target = UnitName('target') if npcscan.target(name) then -- if UnitPlayerControlled('target') then -- if original_target then -- TargetByName(original_target, true) -- end -- else npcscan.remove_target(UnitName('target')) npcscan.play_sound() if npcscan.flash.animation:playing() then npcscan.flash.animation:stop_after(3) else npcscan.flash:reset() npcscan.flash.animation:play() end npcscan.button:set_target() -- end end end end function npcscan.target(name) TargetByName(name, true) return UnitName('target') and strupper(UnitName('target')) == name end function npcscan.on_event() if event == 'VARIABLES_LOADED' then do local flash = CreateFrame('Frame') npcscan.flash = flash flash:Show() flash:SetAllPoints() flash:SetAlpha(0) flash:SetFrameStrata('FULLSCREEN_DIALOG') local texture = flash:CreateTexture() texture:SetBlendMode('ADD') texture:SetAllPoints() texture:SetTexture([[Interface\FullScreenTextures\LowHealth]]) function flash:reset() self:SetAlpha(0) end local screen_flash_in = npcscan.alpha_animation(flash, 1, .5) local screen_flash_out = npcscan.alpha_animation(flash, -1, .5) local screen_flash_delay = npcscan.delay(.5) flash.animation = (screen_flash_in..screen_flash_delay..screen_flash_out) * 3 end local button = CreateFrame('Button', 'npcscan_button', UIParent) button:Hide() npcscan.button = button button:SetPoint('BOTTOM', UIParent, 0, 128) button:SetWidth(150) button:SetHeight(42) button:SetScale(1.25) button:SetFrameStrata('FULLSCREEN_DIALOG') button:SetNormalTexture([[Interface\AddOns\npcscan\UI-Achievement-Parchment-Horizontal]]) button:SetBackdrop{ tile = true, edgeSize = 16, edgeFile = [[Interface\Tooltips\UI-Tooltip-Border]], } button:SetBackdropBorderColor(unpack(BROWN)) button:SetScript('OnEnter', function() this:SetBackdropBorderColor(unpack(YELLOW)) end) button:SetScript('OnLeave', function() this:SetBackdropBorderColor(unpack(BROWN)) end) button:SetScript('OnClick', function() TargetByName(this:GetText(), true) end) function button:set_target() self:SetText(UnitName('target')) self.model:reset() self.model:SetUnit('target') self:Show() self.glow.animation:stop() self.shine.animation:stop() self.glow:reset() self.shine:reset() self.glow.animation:play() self.shine.animation:play() end do local background = button:GetNormalTexture() background:SetDrawLayer('BACKGROUND') background:ClearAllPoints() background:SetPoint('BOTTOMLEFT', 3, 3) background:SetPoint('TOPRIGHT', -3, -3) background:SetTexCoord(0, 1, 0, 0.25) end do local title_background = button:CreateTexture(nil, 'BORDER') title_background:SetTexture([[Interface\AddOns\npcscan\UI-Achievement-Title]]) title_background:SetPoint('TOPRIGHT', -5, -5) title_background:SetPoint('LEFT', 5, 0) title_background:SetHeight(18) title_background:SetTexCoord(0, .9765625, 0, .3125) title_background:SetAlpha(.8) local title = button:CreateFontString(nil, 'OVERLAY', 'GameFontHighlightMedium') title:SetPoint('TOPLEFT', title_background, 0, 0) title:SetPoint('RIGHT', title_background) button:SetFontString(title) local subtitle = button:CreateFontString(nil, 'OVERLAY', 'GameFontBlackTiny') subtitle:SetPoint('TOPLEFT', title, 'BOTTOMLEFT', 0, -4) subtitle:SetPoint('RIGHT', title ) subtitle:SetText('NPC Found!') end do local model = CreateFrame('PlayerModel', nil, button) button.model = model model:SetPoint('BOTTOMLEFT', button, 'TOPLEFT', 0, -4) model:SetPoint('RIGHT', 0, 0) model:SetHeight(button:GetWidth() * .6) do local last_update, delay function model:on_update() this:SetFacing(this:GetFacing() + (GetTime() - last_update) * math.pi / 4) last_update = GetTime() end function model:on_update_model() if delay > 0 then delay = delay - 1 return end this:SetScript('OnUpdateModel', nil) this:SetScript('OnUpdate', this.on_update) this:SetModelScale(.75) this:SetAlpha(1) last_update = GetTime() end function model:reset() self:SetAlpha(0) self:SetFacing(0) self:SetModelScale(1) self:ClearModel() self:SetScript('OnUpdate', nil) self:SetScript("OnUpdateModel", self.on_update_model) delay = 10 -- to prevent scaling bugs end end end do local close = CreateFrame('Button', nil, button, 'UIPanelCloseButton') close:SetPoint('TOPRIGHT', 0, 0) close:SetWidth(32) close:SetHeight(32) close:SetScale(.8) close:SetHitRectInsets(8, 8, 8, 8) end do local glow = button:CreateTexture(nil, 'OVERLAY') button.glow = glow glow:SetPoint('CENTER', button, 'CENTER') glow:SetWidth(400 / 300 * button:GetWidth()) glow:SetHeight(171 / 70 * button:GetHeight()) glow:SetTexture([[Interface\AddOns\npcscan\UI-Achievement-Alert-Glow]]) glow:SetBlendMode('ADD') glow:SetTexCoord(0, .78125, 0, .66796875) glow:SetAlpha(0) function glow:reset() self:SetAlpha(0) end local glow_in = npcscan.alpha_animation(glow, 1, .2) local glow_out = npcscan.alpha_animation(glow, -1, .5) glow.animation = glow_in..glow_out end do local shine = button:CreateTexture(nil, 'ARTWORK') button.shine = shine shine:SetPoint('TOPLEFT', button, 0, 8) shine:SetWidth(67 / 300 * button:GetWidth()) shine:SetHeight(1.28 * button:GetHeight()) shine:SetTexture([[Interface\AddOns\npcscan\UI-Achievement-Alert-Glow]]) shine:SetBlendMode('ADD') shine:SetTexCoord(.78125, .912109375, 0, .28125) shine:SetAlpha(0) function shine:reset() self:SetAlpha(0) self:SetPoint('TOPLEFT', button, 0, 8) end local shine_delay1 = npcscan.delay(.3) local shine_in = npcscan.alpha_animation(shine, 1, 0) local shine_delay2 = npcscan.delay(.2) local shine_move = npcscan.translation_animation(shine, button:GetWidth() - shine:GetWidth() + 8, 0, .4) local shine_out = npcscan.alpha_animation(shine, -1, .2) shine.animation = (shine_delay1..shine_in)..(shine_move + (shine_delay2..shine_out)) end end end do npcscan.last_check = GetTime() function npcscan.on_update() if GetTime() - npcscan.last_check >= CHECK_INTERVAL then npcscan.last_check = GetTime() npcscan.without_errors(npcscan.check_for_targets) end end end function npcscan.log(msg) if DEFAULT_CHAT_FRAME then DEFAULT_CHAT_FRAME:AddMessage('[npcscan]: '..msg, 1, 1, 0) end end -- Slash commands function npcscan.sorted_targets() local sorted_targets = {} for key, _ in pairs(npcscan_targets) do tinsert(sorted_targets, key) end sort(sorted_targets, function(key1, key2) return key1 < key2 end) return sorted_targets end function npcscan.add_target(name) local key = strupper(name) if key ~= '' and not npcscan_targets[key] then npcscan_targets[key] = true npcscan.log(key..' (added)') end end function npcscan.remove_target(name) local key = strupper(name) if npcscan_targets[key] then npcscan_targets[key] = nil npcscan.log(key..' (removed)') end end SLASH_NPCSCAN1 = '/npcscan' function SlashCmdList.NPCSCAN(parameter) local _, _, command, name = strfind(parameter, '%s*(%a*)%s*(.*)') if command == '' then for _, key in ipairs(npcscan.sorted_targets()) do npcscan.log(key) end elseif command == 'add' then npcscan.add_target(name) elseif command == 'rem' then npcscan.remove_target(name) end end -- Animation framework function npcscan.translation_animation(target, x_change, y_change, duration) return npcscan.animation( {x_change, y_change}, duration, function() local _, _, _, original_x, original_y = target:GetPoint() return original_x, original_y end, function(x, y) local point, relative_to, relative_point = target:GetPoint() target:SetPoint( point, relative_to, relative_point, x, y ) end ) end function npcscan.alpha_animation(target, change, duration) return npcscan.animation( {change}, duration, function() return target:GetAlpha() end, function(alpha) target:SetAlpha(alpha) end ) end function npcscan.delay(t) return npcscan.animation({}, t, function() end, function() end) end do local animation_metamethods = {} local mul_mt = { __index = {} } local add_mt = { __index = {} } local concat_mt = { __index = {} } local animation_mt = { __index = {} } function animation_metamethods.__mul(animation, repetitions) local self = { _frame = CreateFrame('Frame'), _repetitions_left = 0, _repetitions = repetitions, _animation = animation, } setmetatable(self, mul_mt) self._frame:SetScript('OnUpdate', function() self:_on_update() end) return self end function animation_metamethods.__add(lhs, rhs) local self = {} setmetatable(self, add_mt) self._animations = {lhs, rhs} return self end function animation_metamethods.__concat(lhs, rhs) local self = { _frame = CreateFrame('Frame') } setmetatable(self, concat_mt) self._animations = {lhs, rhs} self._frame:SetScript('OnUpdate', function() self:_on_update() end) return self end for _, mt in ipairs({ mul_mt, add_mt, concat_mt, animation_mt }) do for key, metamethod in pairs(animation_metamethods) do mt[key] = metamethod end end function mul_mt.__index:play() self._repetitions_left = self._repetitions end function mul_mt.__index:playing() return self._repetitions_left > 0 end function mul_mt.__index:stop() self._repetitions_left = 0 end function mul_mt.__index:stop_after(repetitions) self._repetitions_left = self._repetitions end function mul_mt.__index:_on_update() if self._repetitions_left > 0 then if not self._animation_started then self._animation:play() self._animation_started = true elseif not self._animation:playing() then self._repetitions_left = self._repetitions_left - 1 self._animation_started = false end end end function add_mt.__index:play() for _, animation in ipairs(self._animations) do animation:play() end end function add_mt.__index:playing() for _, animation in ipairs(self._animations) do if animation:playing() then return true end end end function add_mt.__index:stop() for _, animation in ipairs(self._animations) do animation:stop() end end function concat_mt.__index:play() self._index = 1 end function concat_mt.__index:playing() return self._index ~= nil end function concat_mt.__index:stop() if self._index and self._animations[self._index] then self._animations[self._index]:stop() end self._index = nil end function concat_mt.__index:_on_update() if self._index then if not self._animations[self._index] then self._index = nil elseif not self._animation_started then self._animations[self._index]:play() self._animation_started = true elseif not self._animations[self._index]:playing() then self._index = self._index + 1 self._animation_started = false end end end function animation_mt.__index:play() self._t0 = GetTime() self._original_values = { self._getter() } end function animation_mt.__index:stop() self._t0 = nil end function animation_mt.__index:playing() return self._t0 ~= nil end function animation_mt.__index:_current_changes(factor) local values = {} for i, change in ipairs(self._changes) do tinsert(values, self._original_values[i] + change * factor) end return unpack(values) end function animation_mt.__index:_on_update(factor) if self._t0 then local progress = GetTime() - self._t0 if progress >= self._duration then self._callback(self:_current_changes(1)) self._t0 = nil else self._callback(self:_current_changes(progress / self._duration)) end end end function npcscan.animation(changes, duration, getter, callback) local self = { _frame = CreateFrame('Frame'), _changes = changes, _duration = duration, _getter = getter, _callback = callback, } setmetatable(self, animation_mt) self._frame:SetScript('OnUpdate', function() self:_on_update() end) return self end end -- building the frame structure