diff --git a/InnerFireWatch_WotLK.lua b/InnerFireWatch_WotLK.lua new file mode 100644 index 0000000..860bb52 --- /dev/null +++ b/InnerFireWatch_WotLK.lua @@ -0,0 +1,470 @@ +-- InnerFireWatch (WoW 3.3.5 / Epoch) +-- Clean build v2.0 + +InnerFireWatchDB = InnerFireWatchDB or {} + +local frame = CreateFrame("Frame") +local optionsFrame + +local COLOR_PRESETS = { + { key = "white", text = "White", r = 1.0, g = 1.0, b = 1.0 }, + { key = "green", text = "Green", r = 0.0, g = 1.0, b = 0.0 }, + { key = "blue", text = "Blue", r = 0.0, g = 0.4, b = 1.0 }, + { key = "red", text = "Red", r = 1.0, g = 0.0, b = 0.0 }, + { key = "yellow", text = "Yellow", r = 1.0, g = 1.0, b = 0.0 }, + { key = "purple", text = "Purple", r = 0.7, g = 0.0, b = 1.0 }, +} + +local CLASS_COLORS_IFW = { + WARRIOR = { r = 0.78, g = 0.61, b = 0.43 }, + MAGE = { r = 0.41, g = 0.80, b = 0.94 }, + PRIEST = { r = 1.00, g = 1.00, b = 1.00 }, + SHAMAN = { r = 0.00, g = 0.44, b = 0.87 }, +} + +local DEFAULTS = { + soundEnabled = true, + useCustomSound = false, + gainedMessage = false, + largeMessageEnabled = true, + largeMessageR = 1, + largeMessageG = 0, + largeMessageB = 0, + colorPreset = "red", + customSoundPath = "Interface\\AddOns\\InnerFireWatch_WotLK\\sounds\\expire.wav", + fallbackSound = "RaidWarning", +} + +local trackers = {} + +local function ApplyDefaults() + for k, v in pairs(DEFAULTS) do + if InnerFireWatchDB[k] == nil then + InnerFireWatchDB[k] = v + end + end +end + +local function SetColorPresetByKey(key) + local i + for i = 1, #COLOR_PRESETS do + local p = COLOR_PRESETS[i] + if p.key == key then + InnerFireWatchDB.colorPreset = p.key + InnerFireWatchDB.largeMessageR = p.r + InnerFireWatchDB.largeMessageG = p.g + InnerFireWatchDB.largeMessageB = p.b + return + end + end +end + +local function GetCurrentColorPresetText() + local i + for i = 1, #COLOR_PRESETS do + if COLOR_PRESETS[i].key == InnerFireWatchDB.colorPreset then + return COLOR_PRESETS[i].text + end + end + return "Red" +end + +local function GetDetectedClassText() + local className = UnitClass("player") + return className or "Unknown" +end + +local function GetDetectedClassColor() + local _, classToken = UnitClass("player") + if classToken and CLASS_COLORS_IFW[classToken] then + return CLASS_COLORS_IFW[classToken].r, CLASS_COLORS_IFW[classToken].g, CLASS_COLORS_IFW[classToken].b + end + return 0.8, 0.8, 0.8 +end + +local function ClassHasFeatures() + local _, classToken = UnitClass("player") + return classToken == "PRIEST" or classToken == "WARRIOR" or classToken == "SHAMAN" or classToken == "MAGE" +end + +-- Large center message +local CenterAlert = CreateFrame("Frame", "InnerFireWatchCenterAlert", UIParent) +CenterAlert:SetPoint("CENTER", UIParent, "CENTER", 0, 120) +CenterAlert:SetWidth(700) +CenterAlert:SetHeight(90) +CenterAlert:Hide() + +CenterAlert.text = CenterAlert:CreateFontString(nil, "OVERLAY") +CenterAlert.text:SetAllPoints(CenterAlert) +CenterAlert.text:SetFont("Fonts\\FRIZQT__.TTF", 32, "OUTLINE") +CenterAlert.text:SetJustifyH("CENTER") +CenterAlert.text:SetJustifyV("MIDDLE") + +local function ShowCenterAlert(msg) + CenterAlert:SetAlpha(1) + CenterAlert.text:SetText(msg or "") + CenterAlert.text:SetTextColor(InnerFireWatchDB.largeMessageR, InnerFireWatchDB.largeMessageG, InnerFireWatchDB.largeMessageB) + CenterAlert:Show() + CenterAlert.startTime = GetTime() +end + +CenterAlert:SetScript("OnUpdate", function(self) + if not self:IsShown() then return end + + local elapsed = GetTime() - (self.startTime or 0) + if elapsed >= 2.5 then + self:Hide() + return + end + + self:SetAlpha(1 - (elapsed / 2.5)) +end) + +local function PlayExpireSound() + if not InnerFireWatchDB.soundEnabled then return end + + if InnerFireWatchDB.useCustomSound then + if PlaySoundFile then + PlaySoundFile(InnerFireWatchDB.customSoundPath) + end + return + end + + if PlaySound then + PlaySound(InnerFireWatchDB.fallbackSound) + end +end + +local function NotifyExpired(buffLabel) + local msg = buffLabel .. " has expired!" + + DEFAULT_CHAT_FRAME:AddMessage("|cffff3333" .. msg .. "|r") + + if InnerFireWatchDB.largeMessageEnabled then + ShowCenterAlert(string.upper(buffLabel) .. " EXPIRED!") + end + + if UIErrorsFrame and UIErrorsFrame.AddMessage then + UIErrorsFrame:AddMessage(msg, 1.0, 0.1, 0.1, 53, 5) + end + + PlayExpireSound() +end + +local function NotifyGained(buffLabel) + if not InnerFireWatchDB.gainedMessage then return end + DEFAULT_CHAT_FRAME:AddMessage("|cff33ff33" .. buffLabel .. " active.|r") +end + +local function HasBuffByName(unit, buffName) + local i = 1 + while true do + local name = UnitBuff(unit, i) + if not name then + break + end + + if name == buffName then + return true + end + + i = i + 1 + end + + return false +end + +local function BuildTrackers() + trackers = {} + + local _, classToken = UnitClass("player") + + if classToken == "PRIEST" then + table.insert(trackers, { label = "Inner Fire", name = "Inner Fire", active = false }) + elseif classToken == "WARRIOR" then + table.insert(trackers, { label = "Battle Shout", name = "Battle Shout", active = false }) + elseif classToken == "SHAMAN" then + table.insert(trackers, { label = "Lightning Shield", name = "Lightning Shield", active = false }) + table.insert(trackers, { label = "Water Shield", name = "Water Shield", active = false }) + table.insert(trackers, { label = "Earth Shield", name = "Earth Shield", active = false }) + elseif classToken == "MAGE" then + table.insert(trackers, { label = "Arcane Intellect", name = "Arcane Intellect", active = false }) + table.insert(trackers, { label = "Ice Armor", name = "Ice Armor", active = false }) + table.insert(trackers, { label = "Mage Armor", name = "Mage Armor", active = false }) + end + + local i + for i = 1, #trackers do + trackers[i].active = HasBuffByName("player", trackers[i].name) + end +end + +local function CheckBuffs() + if not trackers or #trackers == 0 then return end + + local i + for i = 1, #trackers do + local t = trackers[i] + local nowActive = HasBuffByName("player", t.name) + + if t.active and not nowActive then + NotifyExpired(t.label) + elseif (not t.active) and nowActive then + NotifyGained(t.label) + end + + t.active = nowActive + end +end + +-- Options UI helpers +local function ApplyOptionsBackdrop(parent) + if parent.bg then return end + + parent.bg = parent:CreateTexture(nil, "BACKGROUND") + parent.bg:SetPoint("TOPLEFT", parent, "TOPLEFT", 8, -8) + parent.bg:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -8, 8) + parent.bg:SetTexture(0, 0, 0, 0.90) + + parent.border = CreateFrame("Frame", nil, parent) + parent.border:SetPoint("TOPLEFT", parent, "TOPLEFT", 4, -4) + parent.border:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -4, 4) + parent.border:SetBackdrop({ + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 }, + }) +end + +local function CreateCheckbox(parent, labelText, x, y, getter, setter) + local cb = CreateFrame("CheckButton", nil, parent, "UICheckButtonTemplate") + cb:SetWidth(24) + cb:SetHeight(24) + cb:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + + local text = parent:CreateFontString(nil, "ARTWORK", "GameFontNormal") + text:SetPoint("LEFT", cb, "RIGHT", 4, 0) + text:SetText(labelText) + + cb:SetScript("OnClick", function(self) + setter(self:GetChecked() and true or false) + end) + + cb.Refresh = function(self) + self:SetChecked(getter() and true or false) + end + + return cb +end + +local function RefreshOptionsUI() + if not optionsFrame then return end + + if optionsFrame.classText then + optionsFrame.classText:SetText("Class Detected: " .. GetDetectedClassText()) + optionsFrame.classText:SetTextColor(GetDetectedClassColor()) + end + + if optionsFrame.noFeaturesText then + if ClassHasFeatures() then + optionsFrame.noFeaturesText:SetText("") + else + optionsFrame.noFeaturesText:SetText("No features are available for this class.") + end + end + + if optionsFrame.soundCB then optionsFrame.soundCB:Refresh() end + if optionsFrame.customSoundCB then optionsFrame.customSoundCB:Refresh() end + if optionsFrame.gainedCB then optionsFrame.gainedCB:Refresh() end + if optionsFrame.largeCB then optionsFrame.largeCB:Refresh() end + if optionsFrame.colorText then optionsFrame.colorText:SetText(GetCurrentColorPresetText()) end +end + +local function CreateOptionsWindow() + if optionsFrame then return end + + local f = CreateFrame("Frame", "InnerFireWatchOptionsFrame", UIParent) + f:SetWidth(360) + f:SetHeight(385) + f:SetPoint("CENTER", UIParent, "CENTER", 0, 0) + f:SetFrameStrata("DIALOG") + f:SetFrameLevel(20) + f:EnableMouse(true) + f:SetMovable(true) + f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function(self) self:StartMoving() end) + f:SetScript("OnDragStop", function(self) self:StopMovingOrSizing() end) + f:Hide() + + ApplyOptionsBackdrop(f) + table.insert(UISpecialFrames, "InnerFireWatchOptionsFrame") + + local title = f:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge") + title:SetPoint("TOP", f, "TOP", 0, -14) + title:SetText("InnerFireWatch") + + local subtitle = f:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") + subtitle:SetPoint("TOP", title, "BOTTOM", 0, -4) + subtitle:SetText("Made for Epoch") + + local close = CreateFrame("Button", nil, f, "UIPanelCloseButton") + close:SetPoint("TOPRIGHT", f, "TOPRIGHT", -4, -4) + + local x = 22 + local y = -52 + local rowGap = 10 + + f.classText = f:CreateFontString(nil, "ARTWORK", "GameFontNormal") + f.classText:SetPoint("TOPLEFT", f, "TOPLEFT", x, y) + f.classText:SetText("Class Detected: ") + y = y - 12 - 4 + + f.noFeaturesText = f:CreateFontString(nil, "ARTWORK", "GameFontRedSmall") + f.noFeaturesText:SetPoint("TOPLEFT", f, "TOPLEFT", x, y) + f.noFeaturesText:SetText("") + y = y - 14 - rowGap + + f.soundCB = CreateCheckbox(f, "Enable sound", x - 4, y, function() + return InnerFireWatchDB.soundEnabled + end, function(value) + InnerFireWatchDB.soundEnabled = value + end) + y = y - 24 - rowGap + + f.customSoundCB = CreateCheckbox(f, "Use Custom Sound File", x - 4, y, function() + return InnerFireWatchDB.useCustomSound + end, function(value) + InnerFireWatchDB.useCustomSound = value + end) + y = y - 24 - 4 + + local soundNote = f:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") + soundNote:SetPoint("TOPLEFT", f, "TOPLEFT", x, y) + soundNote:SetText("Sounds/expire.wav file in AddOn Folder") + y = y - 14 - rowGap + + local testSound = CreateFrame("Button", nil, f, "UIPanelButtonTemplate") + testSound:SetWidth(90) + testSound:SetHeight(22) + testSound:SetPoint("TOPLEFT", f, "TOPLEFT", x - 4, y) + testSound:SetText("Test Sound") + testSound:SetScript("OnClick", function() + PlayExpireSound() + end) + y = y - 22 - rowGap + + f.gainedCB = CreateCheckbox(f, "Show gained message", x - 4, y, function() + return InnerFireWatchDB.gainedMessage + end, function(value) + InnerFireWatchDB.gainedMessage = value + end) + y = y - 24 - rowGap + + f.largeCB = CreateCheckbox(f, "Show large center message", x - 4, y, function() + return InnerFireWatchDB.largeMessageEnabled + end, function(value) + InnerFireWatchDB.largeMessageEnabled = value + end) + y = y - 24 - rowGap + + local colorLabel = f:CreateFontString(nil, "ARTWORK", "GameFontNormal") + colorLabel:SetPoint("TOPLEFT", f, "TOPLEFT", x, y) + colorLabel:SetText("Large message color:") + y = y - 30 + + local dd = CreateFrame("Frame", "InnerFireWatchColorDropdown", f, "UIDropDownMenuTemplate") + dd:SetPoint("TOPLEFT", f, "TOPLEFT", x - 12, y + 8) + UIDropDownMenu_SetWidth(dd, 140) + + f.colorText = f:CreateFontString(nil, "ARTWORK", "GameFontHighlight") + f.colorText:SetPoint("LEFT", dd, "RIGHT", -10, 2) + f.colorText:SetText("") + + UIDropDownMenu_Initialize(dd, function(self, level) + local i + for i = 1, #COLOR_PRESETS do + local p = COLOR_PRESETS[i] + local info = UIDropDownMenu_CreateInfo() + info.text = p.text + info.func = function() + SetColorPresetByKey(p.key) + UIDropDownMenu_SetSelectedID(dd, i) + RefreshOptionsUI() + end + UIDropDownMenu_AddButton(info, level) + end + end) + + local i + for i = 1, #COLOR_PRESETS do + if COLOR_PRESETS[i].key == InnerFireWatchDB.colorPreset then + UIDropDownMenu_SetSelectedID(dd, i) + break + end + end + + local preview = CreateFrame("Button", nil, f, "UIPanelButtonTemplate") + preview:SetWidth(90) + preview:SetHeight(22) + preview:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 18, 18) + preview:SetText("Preview") + preview:SetScript("OnClick", function() + ShowCenterAlert("PREVIEW MESSAGE") + PlayExpireSound() + end) + + local closeBtn = CreateFrame("Button", nil, f, "UIPanelButtonTemplate") + closeBtn:SetWidth(120) + closeBtn:SetHeight(22) + closeBtn:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -18, 18) + closeBtn:SetText("Save & Close") + closeBtn:SetScript("OnClick", function() + f:Hide() + end) + + optionsFrame = f + RefreshOptionsUI() +end + +local function ToggleOptionsWindow() + if not optionsFrame then + CreateOptionsWindow() + end + + if optionsFrame:IsShown() then + optionsFrame:Hide() + else + RefreshOptionsUI() + optionsFrame:Show() + end +end + +frame:RegisterEvent("PLAYER_LOGIN") +frame:RegisterEvent("UNIT_AURA") + +frame:SetScript("OnEvent", function(self, event, arg1) + if event == "PLAYER_LOGIN" then + ApplyDefaults() + SetColorPresetByKey(InnerFireWatchDB.colorPreset or "red") + BuildTrackers() + CreateOptionsWindow() + return + end + + if event == "UNIT_AURA" and arg1 == "player" then + CheckBuffs() + end +end) + +SLASH_INNERFIREWATCH1 = "/ifw" +SlashCmdList["INNERFIREWATCH"] = function(msg) + msg = msg and string.lower(msg) or "" + + if msg == "help" then + DEFAULT_CHAT_FRAME:AddMessage("|cff00ccffInnerFireWatch Help|r") + DEFAULT_CHAT_FRAME:AddMessage(" /ifw - Open options window") + DEFAULT_CHAT_FRAME:AddMessage(" /ifw help - Show this message") + else + ToggleOptionsWindow() + end +end diff --git a/InnerFireWatch_WotLK.toc b/InnerFireWatch_WotLK.toc new file mode 100644 index 0000000..7216b4b --- /dev/null +++ b/InnerFireWatch_WotLK.toc @@ -0,0 +1,8 @@ +## Interface: 30300 +## Title: InnerFireWatch +## Notes: Alerts when tracked buffs expire on WoW 3.3.5 / Epoch and includes a small options window. +## Author: Olzon +## Version: 2.0.2 +## SavedVariables: InnerFireWatchDB + +InnerFireWatch_WotLK.lua diff --git a/README.md b/README.md index 222e606..5a6a9a0 100644 --- a/README.md +++ b/README.md @@ -1,74 +1,42 @@ -# InnerFireWatch (Turtle WoW) +# InnerFireWatch -Version: 1.2\ +Version: 2.0 Author: Olzon -A lightweight Turtle WoW (1.12.1) addon that alerts you when important -self-buffs expire. +WoW 3.3.5 / Epoch addon that alerts when tracked buffs expire. ------------------------------------------------------------------------- +## Commands -## Supported Classes & Buffs +- `/ifw` — Open options window +- `/ifw help` — Show help message + +## Current Tracking ### Priest - -- Inner Fire +- Inner Fire ### Warrior - -- Battle Shout +- Battle Shout ### Shaman - -- Lightning Shield -- Water Shield -- Earth Shield +- Lightning Shield +- Water Shield +- Earth Shield ### Mage +- Arcane Intellect +- Ice Armor +- Mage Armor -- Arcane Intellect -- Ice Armor -- Mage Armor +## Options ------------------------------------------------------------------------- +- Enable sound +- Use Custom Sound File +- Test Sound +- Show gained message +- Show large center message +- Color dropdown -## Features +## UI Update -- Detects when a tracked buff expires -- Prints a chat warning -- Displays red UI error text -- Optional large center-screen alert (toggleable) -- Preset color options for large alert -- Large center-screen alert lasts ~2.5s -- Optional sound alert (custom or default) -- Extremely lightweight and vanilla-friendly - ------------------------------------------------------------------------- - -## Slash Commands - -- `/ifw on` -- `/ifw off` -- `/ifw sound on|off` -- `/ifw gained on|off` -- `/ifw large on|off` -- `/ifw white` -- `/ifw green` -- `/ifw blue` -- `/ifw red` -- `/ifw yellow` -- `/ifw purple` - ------------------------------------------------------------------------- - -## Optional: Custom Sound - -Place `expire.wav` inside: - -`Interface\AddOns\InnerFireWatch\sounds\` - -If present, it will be used automatically. - ------------------------------------------------------------------------- - -Designed for Turtle WoW. +- Close button changed to **Save & Close** with increased width for clarity.