diff --git a/AddOns/tullaRange/LICENSE b/AddOns/tullaRange/LICENSE
new file mode 100644
index 0000000..7bd2426
--- /dev/null
+++ b/AddOns/tullaRange/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2010 Jason Greer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
\ No newline at end of file
diff --git a/AddOns/tullaRange/README.md b/AddOns/tullaRange/README.md
new file mode 100644
index 0000000..a0b7245
--- /dev/null
+++ b/AddOns/tullaRange/README.md
@@ -0,0 +1,3 @@
+# tullaRange
+
+An addon for World of Warcraft that makes buttons appear red when out of range
diff --git a/AddOns/tullaRange/changelog.md b/AddOns/tullaRange/changelog.md
new file mode 100644
index 0000000..1939bb8
--- /dev/null
+++ b/AddOns/tullaRange/changelog.md
@@ -0,0 +1,15 @@
+# tullaRange release notes
+
+## 8.2.2
+
+* Added classic build
+
+## 8.2.1
+
+* Automated releases
+
+## 8.2.0
+
+* Updated TOC for 8.2.0
+* Verified the addon works with classic
+* Cleaned up code a tiny bit
diff --git a/AddOns/tullaRange/tullaRange.lua b/AddOns/tullaRange/tullaRange.lua
new file mode 100644
index 0000000..be8eca2
--- /dev/null
+++ b/AddOns/tullaRange/tullaRange.lua
@@ -0,0 +1,324 @@
+--[[
+ tullaRange
+ Adds out of range coloring to action buttons
+ Derived from RedRange with negligable improvements to CPU usage
+--]]
+
+--locals and speed
+local AddonName, Addon = ...
+
+local DB_KEY = 'TULLARANGE_COLORS'
+local UPDATE_DELAY = 0.15
+local ATTACK_BUTTON_FLASH_TIME = _G.ATTACK_BUTTON_FLASH_TIME
+
+local IsActionInRange = _G.IsActionInRange
+local IsUsableAction = _G.IsUsableAction
+local HasAction = _G.HasAction
+
+--[[
+ Helper Functions
+--]]
+
+local function removeDefaults(tbl, defaults)
+ for k, v in pairs(defaults) do
+ if type(tbl[k]) == 'table' and type(v) == 'table' then
+ removeDefaults(tbl[k], v)
+ if next(tbl[k]) == nil then
+ tbl[k] = nil
+ end
+ elseif tbl[k] == v then
+ tbl[k] = nil
+ end
+ end
+
+ return tbl
+end
+
+local function copyDefaults(tbl, defaults)
+ for k, v in pairs(defaults) do
+ if type(v) == 'table' then
+ tbl[k] = copyDefaults(tbl[k] or {}, v)
+ elseif tbl[k] == nil then
+ tbl[k] = v
+ end
+ end
+
+ return tbl
+end
+
+
+--[[
+ The main thing
+--]]
+
+function Addon:Load()
+ self.buttonColors = {}
+ self.buttonsToUpdate = {}
+
+ -- create a frame for watching for the options menu to show up
+ -- when it does, load the options menu
+ do
+ local optionsWatcher = CreateFrame('Frame', nil, InterfaceOptionsFrame)
+
+ optionsWatcher:SetScript('OnShow', function(watcher)
+ watcher:SetScript('OnShow', nil)
+ LoadAddOn(AddonName .. '_Config')
+ end)
+ end
+
+
+ -- create a frame for handling events and throttling timer updates
+ do
+ local eventHandler = CreateFrame('Frame', nil); eventHandler:Hide()
+
+ eventHandler.remain = UPDATE_DELAY
+
+ eventHandler:SetScript('OnEvent', function(handler, ...)
+ self:OnEvent(...)
+ end)
+
+ eventHandler:SetScript('OnUpdate', function(handler, elapsed)
+ local remain = handler.remain - elapsed
+
+ if remain > 0 then
+ handler.remain = remain
+ else
+ handler.remain = UPDATE_DELAY
+
+ if not self:UpdateButtons(UPDATE_DELAY - remain) then
+ handler:Hide()
+ end
+ end
+ end)
+
+ eventHandler:RegisterEvent('PLAYER_LOGIN')
+ eventHandler:RegisterEvent('PLAYER_LOGOUT')
+
+ self.updater = eventHandler
+ end
+
+ --make thyself global
+ _G[AddonName] = self
+end
+
+
+--[[
+ Frame Events
+--]]
+
+function Addon:OnEvent(event, ...)
+ local action = self[event]
+
+ if action then
+ action(self, event, ...)
+ end
+end
+
+function Addon:PLAYER_LOGIN()
+ self:SetupDatabase()
+ self:HookActionEvents()
+end
+
+function Addon:PLAYER_LOGOUT()
+ self:CleanupDatabase()
+end
+
+
+--[[
+ Button Hooking
+--]]
+
+do
+ local function button_UpdateStatus(button)
+ Addon:UpdateButtonStatus(button)
+ end
+
+ local function button_UpdateUsable(button)
+ Addon:UpdateButtonUsable(button, true)
+ end
+
+ local function button_Register(button)
+ Addon:Register(button)
+ end
+
+ function Addon:HookActionEvents()
+ hooksecurefunc('ActionButton_OnUpdate', button_Register)
+ hooksecurefunc('ActionButton_UpdateUsable', button_UpdateUsable)
+ hooksecurefunc('ActionButton_Update', button_UpdateStatus)
+ end
+
+ function Addon:Register(button)
+ button:HookScript('OnShow', button_UpdateStatus)
+ button:HookScript('OnHide', button_UpdateStatus)
+ button:SetScript('OnUpdate', nil)
+
+ self:UpdateButtonStatus(button)
+ end
+end
+
+
+--[[
+ Actions
+--]]
+
+function Addon:RequestUpdate()
+ if next(self.buttonsToUpdate) then
+ self.updater:Show()
+ end
+end
+
+function Addon:UpdateButtons(elapsed)
+ if next(self.buttonsToUpdate) then
+ for button in pairs(self.buttonsToUpdate) do
+ self:UpdateButton(button, elapsed)
+ end
+
+ return true
+ end
+
+ return false
+end
+
+function Addon:UpdateButton(button, elapsed)
+ self:UpdateButtonUsable(button)
+ self:UpdateButtonFlash(button, elapsed)
+end
+
+function Addon:UpdateButtonUsable(button, force)
+ if force then
+ self.buttonColors[button] = nil
+ end
+
+ local action = button.action
+ local isUsable, notEnoughMana = IsUsableAction(action)
+
+ --usable (ignoring target information)
+ if isUsable then
+ local inRange = IsActionInRange(action)
+
+ --but out of range
+ if inRange == false then
+ self:SetButtonColor(button, 'oor')
+ else
+ self:SetButtonColor(button, 'normal')
+ end
+ --out of mana
+ elseif notEnoughMana then
+ self:SetButtonColor(button, 'oom')
+ --unusable
+ else
+ self:SetButtonColor(button, 'unusable')
+ end
+end
+
+function Addon:UpdateButtonFlash(button, elapsed)
+ if button.flashing ~= 1 then return end
+
+ local flashtime = button.flashtime - elapsed
+
+ if flashtime <= 0 then
+ local overtime = -flashtime
+
+ if overtime >= ATTACK_BUTTON_FLASH_TIME then
+ overtime = 0
+ end
+
+ flashtime = ATTACK_BUTTON_FLASH_TIME - overtime
+
+ local flashTexture = button.Flash
+ if flashTexture:IsShown() then
+ flashTexture:Hide()
+ else
+ flashTexture:Show()
+ end
+ end
+
+ button.flashtime = flashtime
+end
+
+function Addon:UpdateButtonStatus(button)
+ local action = button.action
+
+ if action and button:IsVisible() and HasAction(action) then
+ self.buttonsToUpdate[button] = true
+ else
+ self.buttonsToUpdate[button] = nil
+ end
+
+ self:RequestUpdate()
+end
+
+function Addon:SetButtonColor(button, colorIndex)
+ if self.buttonColors[button] == colorIndex then return end
+
+ self.buttonColors[button] = colorIndex
+
+ local r, g, b = self:GetColor(colorIndex)
+ button.icon:SetVertexColor(r, g, b)
+end
+
+
+--[[
+ Configuration
+--]]
+
+function Addon:SetupDatabase()
+ local sets = _G[DB_KEY]
+
+ if not sets then
+ sets = {}
+ _G[DB_KEY] = sets
+ end
+
+ self.sets = copyDefaults(sets, self:GetDatabaseDefaults())
+end
+
+function Addon:CleanupDatabase()
+ local sets = self.sets
+
+ if sets then
+ removeDefaults(sets, self:GetDatabaseDefaults())
+ end
+end
+
+function Addon:GetDatabaseDefaults()
+ return {
+ normal = {1, 1, 1},
+ oor = {1, 0.3, 0.1},
+ oom = {0.1, 0.3, 1},
+ unusable = {0.4, 0.4, 0.4}
+ }
+end
+
+function Addon:ResetDatabase()
+ _G[DB_KEY] = nil
+
+ self:SetupDatabase()
+ self:ForceColorUpdate()
+end
+
+function Addon:SetColor(index, r, g, b)
+ local color = self.sets[index]
+
+ color[1] = r
+ color[2] = g
+ color[3] = b
+
+ self:ForceColorUpdate()
+end
+
+function Addon:GetColor(index)
+ local color = self.sets[index]
+
+ return color[1], color[2], color[3]
+end
+
+function Addon:ForceColorUpdate()
+ for button in pairs(self.buttonsToUpdate) do
+ self:UpdateButtonUsable(button, true)
+ end
+end
+
+
+--[[ Load The Thing ]]--
+Addon:Load()
\ No newline at end of file
diff --git a/AddOns/tullaRange/tullaRange.toc b/AddOns/tullaRange/tullaRange.toc
new file mode 100644
index 0000000..6fa4647
--- /dev/null
+++ b/AddOns/tullaRange/tullaRange.toc
@@ -0,0 +1,7 @@
+## Interface: 11302
+## Title: tullaRange
+## Author: Tuller
+## Notes: Out of range coloring based on RedRange
+## Version: 8.2.2
+## SavedVariables: TULLARANGE_COLORS
+tullaRange.lua
diff --git a/AddOns/tullaRange_Config/colorOptions.lua b/AddOns/tullaRange_Config/colorOptions.lua
new file mode 100644
index 0000000..468c639
--- /dev/null
+++ b/AddOns/tullaRange_Config/colorOptions.lua
@@ -0,0 +1,111 @@
+--[[
+ Frame.lua
+ General Bagnon settings
+--]]
+
+local _, Addon = ...
+local L = Addon.L
+
+local ColorOptions
+do
+ ColorOptions = Addon.OptionsPanel:New(
+ 'tullaRange_ColorOptions',
+ nil,
+ 'tullaRange',
+ L.ColorSettingsTitle
+ )
+
+ -- ColorOptions:Hide()
+
+ Addon.ColorOptions = ColorOptions
+end
+
+local SPACING = 4
+local COLOR_TYPES = {'oor', 'oom', 'unusable'}
+
+--[[
+ Startup
+--]]
+
+function ColorOptions:Load()
+ self:SetScript('OnShow', self.OnShow)
+ self:AddWidgets()
+ self:UpdateWidgets()
+end
+
+
+--[[
+ Frame Events
+--]]
+
+function ColorOptions:OnShow()
+ self:UpdateWidgets()
+end
+
+
+--[[
+ Components
+--]]
+
+function ColorOptions:AddWidgets()
+ local lastSelector = nil
+
+ for i, type in self:GetColorTypes() do
+ local selector = self:CreateColorSelector(type)
+
+ selector:SetHeight(132)
+
+ if i == 1 then
+ selector:SetPoint('TOPLEFT', 12, -84)
+ selector:SetPoint('TOPRIGHT', -12, -84)
+ else
+ selector:SetPoint('TOPLEFT', lastSelector, 'BOTTOMLEFT', 0, -(SPACING + 24))
+ selector:SetPoint('TOPRIGHT', lastSelector, 'BOTTOMRIGHT', 0, -(SPACING + 24))
+ end
+
+ lastSelector = selector
+ end
+end
+
+function ColorOptions:UpdateWidgets()
+ if not self:IsVisible() then
+ return
+ end
+
+ if self.sliders then
+ for _, s in pairs(self.sliders) do
+ s:UpdateValue()
+ end
+ end
+
+ for _, type in self:GetColorTypes() do
+ local selector = self:GetColorSelector(type)
+ selector:UpdateValues()
+ end
+end
+
+function ColorOptions:GetColorTypes()
+ return pairs(COLOR_TYPES)
+end
+
+
+--[[ Color Pickers ]]--
+
+--frame color
+function ColorOptions:CreateColorSelector(type)
+ local selector = Addon.ColorSelector:New(type, self)
+
+ local colorSelectors = self.colorSelectors or {}
+ colorSelectors[type] = selector
+ self.colorSelectors = colorSelectors
+
+ return selector
+end
+
+function ColorOptions:GetColorSelector(type)
+ return self.colorSelectors and self.colorSelectors[type]
+end
+
+--[[ Load the thing ]]--
+
+ColorOptions:Load()
\ No newline at end of file
diff --git a/AddOns/tullaRange_Config/localization/localization.cn.lua b/AddOns/tullaRange_Config/localization/localization.cn.lua
new file mode 100644
index 0000000..5b8a845
--- /dev/null
+++ b/AddOns/tullaRange_Config/localization/localization.cn.lua
@@ -0,0 +1,22 @@
+--[[tullaRange Config Localization - Simplified Chinese by Masini]]
+
+if GetLocale() ~= 'zhCN' then return end
+
+local AddonName, Addon = ...
+local L = Addon.L
+
+L.ColorSettings = '颜色'
+
+L.ColorSettingsTitle = 'tullaRange颜色设置设定'
+
+L.oor = '超出距离'
+
+L.oom = '魔力不足'
+
+L.unusable = '不稳定'
+
+L.Red = '红'
+
+L.Green = '绿'
+
+L.Blue = '蓝'
diff --git a/AddOns/tullaRange_Config/localization/localization.de.lua b/AddOns/tullaRange_Config/localization/localization.de.lua
new file mode 100644
index 0000000..389d2c9
--- /dev/null
+++ b/AddOns/tullaRange_Config/localization/localization.de.lua
@@ -0,0 +1,22 @@
+--[[tullaRange Config Localization - German]]
+
+if GetLocale() ~= 'deDE' then return end
+
+local AddonName, Addon = ...
+local L = Addon.L
+
+L.ColorSettings = 'Farben'
+
+L.ColorSettingsTitle = 'Hier kannst du Farbeinstellungen vornehmen'
+
+L.oor = 'Außer Reichweite'
+
+L.oom = 'Nicht genug Mana'
+
+L.unusable = 'Nicht benutzbar'
+
+L.Red = 'Rot'
+
+L.Green = 'Grün'
+
+L.Blue = 'Blau'
diff --git a/AddOns/tullaRange_Config/localization/localization.it.lua b/AddOns/tullaRange_Config/localization/localization.it.lua
new file mode 100644
index 0000000..fce79e7
--- /dev/null
+++ b/AddOns/tullaRange_Config/localization/localization.it.lua
@@ -0,0 +1,22 @@
+--[[tullaRange Config Localization - Italian]]
+
+if GetLocale() ~= 'itIT' then return end
+
+local AddonName, Addon = ...
+local L = Addon.L
+
+L.ColorSettings = 'Colori'
+
+L.ColorSettingsTitle = 'Impostazioni per la configurazione del colore'
+
+L.oor = 'Bersaglio distante'
+
+L.oom = 'Mana scarso'
+
+L.unusable = 'Non utilizzabile'
+
+L.Red = 'Rosso'
+
+L.Green = 'Verde'
+
+L.Blue = 'Blu'
diff --git a/AddOns/tullaRange_Config/localization/localization.ko.lua b/AddOns/tullaRange_Config/localization/localization.ko.lua
new file mode 100644
index 0000000..368047d
--- /dev/null
+++ b/AddOns/tullaRange_Config/localization/localization.ko.lua
@@ -0,0 +1,20 @@
+if GetLocale() ~= 'koKR' then return end
+
+local AddonName, Addon = ...
+local L = Addon.L
+
+L.ColorSettings = '색상'
+
+L.ColorSettingsTitle = 'tullaRange 색상 구성 설정'
+
+L.oor = '사정거리 벗어남'
+
+L.oom = '마나 부족'
+
+L.unusable = '사용불가'
+
+L.Red = '빨강'
+
+L.Green = '녹색'
+
+L.Blue = '파랑'
diff --git a/AddOns/tullaRange_Config/localization/localization.lua b/AddOns/tullaRange_Config/localization/localization.lua
new file mode 100644
index 0000000..add16d0
--- /dev/null
+++ b/AddOns/tullaRange_Config/localization/localization.lua
@@ -0,0 +1,25 @@
+--[[
+ tullaRangeConfig localization
+--]]
+
+local AddonName, Addon = ...
+
+local L = {
+ ColorSettings = 'Colors',
+
+ ColorSettingsTitle = 'tullaRange color configuration settings',
+
+ oor = 'Out of Range',
+
+ oom = 'Out of Mana',
+
+ unusable = 'Unusable',
+
+ Red = 'Red',
+
+ Green = 'Green',
+
+ Blue = 'Blue'
+}
+
+Addon.L = setmetatable(L, { __index = function(t, k) return k end })
\ No newline at end of file
diff --git a/AddOns/tullaRange_Config/localization/localization.tw.lua b/AddOns/tullaRange_Config/localization/localization.tw.lua
new file mode 100644
index 0000000..7d9cc45
--- /dev/null
+++ b/AddOns/tullaRange_Config/localization/localization.tw.lua
@@ -0,0 +1,22 @@
+--[[tullaRange Config Localization - Traditional Chinese by Masini]]
+
+if GetLocale() ~= 'zhTW' then return end
+
+local AddonName, Addon = ...
+local L = Addon.L
+
+L.ColorSettings = '顏色'
+
+L.ColorSettingsTitle = 'tullaRange顏色設置設定'
+
+L.oor = '超出距離'
+
+L.oom = '魔力不足'
+
+L.unusable = '不穩定'
+
+L.Red = '紅'
+
+L.Green = '綠'
+
+L.Blue = '藍'
diff --git a/AddOns/tullaRange_Config/localization/localization.xml b/AddOns/tullaRange_Config/localization/localization.xml
new file mode 100644
index 0000000..819e2ee
--- /dev/null
+++ b/AddOns/tullaRange_Config/localization/localization.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/AddOns/tullaRange_Config/tullaRange_Config.toc b/AddOns/tullaRange_Config/tullaRange_Config.toc
new file mode 100644
index 0000000..6f976db
--- /dev/null
+++ b/AddOns/tullaRange_Config/tullaRange_Config.toc
@@ -0,0 +1,9 @@
+## Interface: 11302
+## Title: tullaRange Config
+## Notes: GUI based configuration for tullaRange
+## Author: Tuller
+## Dependencies: tullaRange
+## LoadOnDemand: 1
+localization\localization.xml
+widgets\widgets.xml
+colorOptions.lua
diff --git a/AddOns/tullaRange_Config/widgets/classy.lua b/AddOns/tullaRange_Config/widgets/classy.lua
new file mode 100644
index 0000000..00876d6
--- /dev/null
+++ b/AddOns/tullaRange_Config/widgets/classy.lua
@@ -0,0 +1,23 @@
+--[[
+ misc things I need to get my widget framework workingish
+--]]
+
+local _, Addon = ...
+
+Addon.Classy = {
+ New = function(self, frameType, parentClass)
+ local class = CreateFrame(frameType)
+ class.mt = {__index = class}
+
+ if parentClass then
+ class = setmetatable(class, {__index = parentClass})
+ class.super = parentClass
+ end
+
+ class.Bind = function(c, obj)
+ return setmetatable(obj, c.mt)
+ end
+
+ return class
+ end
+}
\ No newline at end of file
diff --git a/AddOns/tullaRange_Config/widgets/colorSelector.lua b/AddOns/tullaRange_Config/widgets/colorSelector.lua
new file mode 100644
index 0000000..ee38894
--- /dev/null
+++ b/AddOns/tullaRange_Config/widgets/colorSelector.lua
@@ -0,0 +1,99 @@
+--[[
+ colorSelector.lua
+ A bagnon color selector
+--]]
+
+local _, Addon = ...
+local L = Addon.L
+local tullaRange = _G.tullaRange
+local ColorSelector = Addon.Classy:New('Frame'); Addon.ColorSelector = ColorSelector
+
+local backdrop = {
+ bgFile = [[Interface\ChatFrame\ChatFrameBackground]],
+ edgeFile = [[Interface\Tooltips\UI-Tooltip-Border]],
+ edgeSize = 16,
+ tile = true, tileSize = 16,
+ insets = {left = 4, right = 4, top = 4, bottom = 4}
+}
+
+local ColorSliders = { 'Red', 'Green', 'Blue' }
+
+
+function ColorSelector:New(colorState, parent)
+ local f = self:Bind(CreateFrame('Frame', parent:GetName() .. '_' .. colorState, parent))
+
+ f:SetBackdrop(backdrop)
+ f:SetBackdropBorderColor(0.4, 0.4, 0.4)
+ f:SetBackdropColor(0, 0, 0, 0.3)
+
+ local t = f:CreateFontString(nil, 'BACKGROUND', 'GameFontHighlightLarge')
+ t:SetPoint('BOTTOMLEFT', f, 'TOPLEFT', 4, 2)
+ t:SetText(L[colorState])
+ f.text = t
+
+ local preview = f:CreateTexture(nil, 'ARTWORK')
+ preview:SetPoint('RIGHT', -16, 0)
+ preview:SetSize(96, 96)
+ f.preview = preview
+
+ -- color sliders
+ local sliders = {}
+ for colorIndex, colorName in ipairs(ColorSliders) do
+ local slider = Addon.Slider:New(L[colorName], f, 0, 100, 1)
+
+ slider.SetSavedValue = function(_, value)
+ tullaRange.sets[colorState][colorIndex] = math.floor(value + 0.5) / 100
+ tullaRange:ForceColorUpdate()
+
+ preview:SetVertexColor(tullaRange:GetColor(colorState))
+ end
+
+ slider.GetSavedValue = function()
+ return tullaRange.sets[colorState][colorIndex] * 100
+ end
+
+ if colorIndex > 1 then
+ slider:SetPoint('TOPLEFT', sliders[colorIndex - 1], 'BOTTOMLEFT', 0, -24)
+ else
+ slider:SetPoint('BOTTOMLEFT', t, 'BOTTOMLEFT', 8, -40)
+ end
+
+ table.insert(sliders, slider)
+ end
+
+
+
+ f.sliders = sliders
+
+ return f
+end
+
+do
+ local spellIcons = {}
+
+ -- generate spell icons
+ do
+ for i = 1, GetNumSpellTabs() do
+ local offset, numSpells = select(3, GetSpellTabInfo(i))
+ local tabEnd = offset + numSpells
+
+ for j = offset, tabEnd - 1 do
+ local texture = GetSpellBookItemTexture(j, 'player')
+ if texture then
+ table.insert(spellIcons, texture)
+ end
+ end
+ end
+ end
+
+ function ColorSelector:UpdateValues()
+
+ local texture = spellIcons[math.random(1, #spellIcons)]
+
+ self.preview:SetTexture(texture)
+
+ for _, slider in pairs(self.sliders) do
+ slider:UpdateValue()
+ end
+ end
+end
diff --git a/AddOns/tullaRange_Config/widgets/optionsPanel.lua b/AddOns/tullaRange_Config/widgets/optionsPanel.lua
new file mode 100644
index 0000000..8375b7f
--- /dev/null
+++ b/AddOns/tullaRange_Config/widgets/optionsPanel.lua
@@ -0,0 +1,34 @@
+--[[
+ optionsPanel.lua
+ A bagnon options panel
+--]]
+
+local _, Addon = ...
+local OptionsPanel = Addon.Classy:New('Frame'); Addon.OptionsPanel = OptionsPanel
+
+function OptionsPanel:New(name, parent, title, subtitle, icon)
+ local f = self:Bind(CreateFrame('Frame', name))
+ f.name = title
+ f.parent = parent
+
+ local text = f:CreateFontString(nil, 'ARTWORK', 'GameFontNormalLarge')
+ text:SetPoint('TOPLEFT', 16, -16)
+ if icon then
+ text:SetFormattedText('|T%s:%d|t %s', icon, 32, title)
+ else
+ text:SetText(title)
+ end
+
+ local subtext = f:CreateFontString(nil, 'ARTWORK', 'GameFontHighlightSmall')
+ subtext:SetHeight(32)
+ subtext:SetPoint('TOPLEFT', text, 'BOTTOMLEFT', 0, -8)
+ subtext:SetPoint('RIGHT', f, -32, 0)
+ subtext:SetNonSpaceWrap(true)
+ subtext:SetJustifyH('LEFT')
+ subtext:SetJustifyV('TOP')
+ subtext:SetText(subtitle)
+
+ InterfaceOptions_AddCategory(f)
+
+ return f
+end
\ No newline at end of file
diff --git a/AddOns/tullaRange_Config/widgets/slider.lua b/AddOns/tullaRange_Config/widgets/slider.lua
new file mode 100644
index 0000000..faf7323
--- /dev/null
+++ b/AddOns/tullaRange_Config/widgets/slider.lua
@@ -0,0 +1,98 @@
+--[[
+ slider.lua
+ A options slider
+--]]
+
+local _, Addon = ...
+local Slider = Addon.Classy:New('Slider'); Addon.Slider = Slider
+
+--[[ Constructor ]]--
+
+function Slider:New(name, parent, low, high, step)
+ local f = self:Bind(CreateFrame('Slider', parent:GetName() .. '_' .. name, parent, 'OptionsSliderTemplate'))
+ f:SetMinMaxValues(low, high)
+ f:SetValueStep(step)
+ f:EnableMouseWheel(true)
+
+ _G[f:GetName() .. 'Text']:SetText(name)
+ _G[f:GetName() .. 'Text']:SetFontObject('GameFontNormalLeft')
+ _G[f:GetName() .. 'Text']:ClearAllPoints()
+ _G[f:GetName() .. 'Text']:SetPoint('BOTTOMLEFT', f, 'TOPLEFT')
+ _G[f:GetName() .. 'Low']:SetText('')
+ _G[f:GetName() .. 'High']:SetText('')
+
+ local text = f:CreateFontString(nil, 'BACKGROUND', 'GameFontHighlightSmall')
+ text:SetJustifyH('RIGHT')
+ text:SetPoint('BOTTOMRIGHT', f, 'TOPRIGHT')
+ f.valText = text
+
+ f:SetScript('OnShow', f.OnShow)
+ f:SetScript('OnMouseWheel', f.OnMouseWheel)
+ f:SetScript('OnValueChanged', f.OnValueChanged)
+ f:SetScript('OnMouseWheel', f.OnMouseWheel)
+ f:SetScript('OnEnter', f.OnEnter)
+ f:SetScript('OnLeave', f.OnLeave)
+
+ return f
+end
+
+
+--[[ Frame Events ]]--
+
+function Slider:OnShow()
+ self:UpdateValue()
+end
+
+function Slider:OnValueChanged(value)
+ self:SetSavedValue(value)
+ self:UpdateText(self:GetSavedValue())
+end
+
+function Slider:OnMouseWheel(direction)
+ local step = self:GetValueStep() * direction
+ local value = self:GetValue()
+ local minVal, maxVal = self:GetMinMaxValues()
+
+ if step > 0 then
+ self:SetValue(math.min(value + step, maxVal))
+ else
+ self:SetValue(math.max(value + step, minVal))
+ end
+end
+
+function Slider:OnEnter()
+ if not GameTooltip:IsOwned(self) and self.tooltip then
+ GameTooltip:SetOwner(self, 'ANCHOR_RIGHT')
+ GameTooltip:SetText(self.tooltip)
+ end
+end
+
+function Slider:OnLeave()
+ if GameTooltip:IsOwned(self) then
+ GameTooltip:Hide()
+ end
+end
+
+
+--[[ Update Methods ]]--
+
+function Slider:SetSavedValue(value)
+ assert(false, 'Hey, you forgot to set SetSavedValue for ' .. self:GetName())
+end
+
+function Slider:GetSavedValue()
+ assert(false, 'Hey, you forgot to set GetSavedValue for ' .. self:GetName())
+end
+
+function Slider:UpdateValue()
+ self:SetValue(self:GetSavedValue())
+ self:UpdateText(self:GetSavedValue())
+end
+
+function Slider:UpdateText(value)
+ if self.GetFormattedText then
+ self.valText:SetText(self:GetFormattedText(value))
+ else
+ self.valText:SetText(value)
+ end
+end
\ No newline at end of file
diff --git a/AddOns/tullaRange_Config/widgets/widgets.xml b/AddOns/tullaRange_Config/widgets/widgets.xml
new file mode 100644
index 0000000..761a95b
--- /dev/null
+++ b/AddOns/tullaRange_Config/widgets/widgets.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index e3a753d..528c7e3 100644
--- a/README.md
+++ b/README.md
@@ -26,6 +26,7 @@ v1.13.2怀旧服
- [Questie](https://www.curseforge.com/wow/addons/questie) 任务增强,在地图上显示任务目标和可接任务等。
- [QuestLogEx](https://www.wowinterface.com/downloads/info24980-QuestLogEx.html) 将任务列表和任务详情展示在一个面板上,就像正式服。
- [RealMobHealth](https://www.wowinterface.com/downloads/info24924-RealMobHealth.html) 显示怪物真实(不完全正确)血量,原理是根据伤害和百分比计算出来的,并在玩家间共享,用的时间越长应该越正确。
+- [tullaRange](https://www.curseforge.com/wow/addons/tullarange) 技能icon超出距离和魔法不足着色。
- [WeakAuras](https://www.curseforge.com/wow/addons/weakauras-2) 包含的功能较多,还没用明白。
- [WeaponSwingTimer](https://www.curseforge.com/wow/addons/weaponswingtimer) 平A主副手武器CD监控。
- [WhatsTraining](https://www.wowinterface.com/downloads/info25031-WhatsTraining.html) 技能书增加一个tab,告诉你哪些技能什么等级可学,不用造访技能训练师就能知道,方便练级时使用。