diff --git a/.claude/settings.json b/.claude/settings.json index d1180b8..9659bc0 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,11 +1,30 @@ { "permissions": { + "allow": [ + "Bash(gh issue list:*)", + "Bash(gh issue view:*)", + "Bash(gh pr list:*)", + "Bash(gh pr view:*)", + "Bash(gh pr diff:*)", + "Bash(gh repo view:*)" + ], "deny": [ - "Bash(git push:*)", - "Bash(git commit:*)", + "Bash(gh issue close:*)", + "Bash(gh issue comment:*)", + "Bash(gh issue edit:*)", + "Bash(gh issue create:*)", + "Bash(gh issue delete:*)", + "Bash(gh issue reopen:*)", + "Bash(gh pr merge:*)", + "Bash(gh pr comment:*)", + "Bash(gh pr create:*)", + "Bash(gh pr edit:*)", + "Bash(gh pr close:*)", + "Bash(gh api:*)", "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(git push:*)", "Bash(git submodule update:*)", - "Bash(gh:*)", "Edit(~/.github/addons/refs/**)" ], "additionalDirectories": [ diff --git a/.gitignore b/.gitignore index 677ec37..f36bb04 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ -# IDE -.vscode/ -.idea/ -*.iml - -# Files -docs/issues/ +# IDE +.vscode/ +.idea/ +*.iml + +# Files +docs/issues/ diff --git a/.gitmodules b/.gitmodules index 1d046a4..1314ff8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,15 +1,7 @@ -[submodule "libs/LibStub"] - path = libs/LibStub - url = https://github.com/lua-wow/LibStub.git - branch = master -[submodule "libs/CallbackHandler-1.0"] - path = libs/CallbackHandler-1.0 - url = https://github.com/lua-wow/CallbackHandler-1.0.git - branch = master -[submodule "libs/LibClassicDurations"] - path = libs/LibClassicDurations - url = https://github.com/rgd87/LibClassicDurations.git - branch = master +[submodule "libs/LibStub"] + path = libs/LibStub + url = https://github.com/lua-wow/LibStub.git + branch = master [submodule "libs/LibDispel"] path = libs/LibDispel url = https://github.com/lua-wow/LibDispel.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 3974105..ae3c5e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,229 +1,233 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -## [5.3.0] - 2026-10-19 - -### Added - -- Support for Midnight (12.0) secret values: restricted auras are looked up by listed spell ids, secret cooldowns use duration objects -- Support for Classic Forever, TBC Anniversary and Titan WotLK clients -- Spells for Mists of Pandaria Classic -- CI: luacheck lint, interface version bump, packager release - -### Changed - -- Single `Filger.toc` for every client; removed `Filger_Classic.toc` and `Filger_Mists.toc` -- Spell data split into per-client `data/` folders, loaded through the TOC `[Game]` variable -- Replaced deprecated item/spell globals with `C_Item`, `C_Container`, `C_SpellBook` and `C_Spell` -- Debuff colors from `AuraUtil` -- Updated libraries (LibStub, CallbackHandler-1.0, LibDispel) - -### Removed - -- Cataclysm Classic from the TOC - -### Fixed - -- luacheck warnings, leaked `numVisible` and `itemType` globals - -## [5.2.1] - 2025-03-08 - -### Added - -- Support for classic era -- Spells updates - -### Changed - -- Update classic warrior cooldown -- Added classic elixirs to blacklist - -## [5.2.0] - 2024-10-29 - -### Added - -- Update shaman enhancement spells and cooldowns -- Added items spells priority - -### Changed - -- Update spell priority -- Update processing and filtering spells - -## [5.1.1] - 2024-10-07 - -### Changed - -- replace `_G.IsAddOnLoaded` with `C_AddOns.IsAddOnLoaded` (patch 11.0.2) - -## [5.1.0] - 2024-08-04 - -### Changed - -- Update spells for patch 11.0.0 -- Added cataclysm spells -- Fix cataclysm cooldowns -- Fix dispelable aura highlight - -## [5.0.0] - 2024-07-27 - -### Changed - -- Complete rework -- Update `UNIT_AURA` event to use `UnitAuraInfo` -- Update API to use `C_Spell` namespace. -- Added new lib `LibDispel`. -- Fixed aura colors and dispelable/stealable animation. - -## [4.3.2] - 2024-07-26 - -### Changed - -- Update .toc files -- Update submodules - -## [4.3.1] - 2024-01-13 - -### Changed - -- Adding `LibStub` as git submodule -- Adding `CallbackHandler-1.0` as git submodule -- Adding `LibClassicDurations` as git submodule - -## [4.3.0] - 2024-01-13 - -### Changed - -- Changed Tukui dependency from `required` to `optional` -- Fixed `CreateBackdrop` error, caused by required depencies - -## [4.2] - 2024-01-02 - -### Added - -- `CHANGELOG.mf' initialized. - -## [2.5] - 24/10/2019 - -### Changed - -- Patch 8.2.5 - -## [2.4] - 06/03/2019 - -### Added - -- WoLK dungeons and raid tables. -- Update BoA dungeon and raid tables. -- Tracer plugins (help to build spell tables by saving buffs/debuffs that affect player) - -### Changed - -- Tracer plugin enhanced (added automatic option). - -## [2.3] - 04/02/2019 - -### Changed - -- Fixed icon cooldown countdown double text. -- Fixed timer text for auras with no expiration time. - -## [2.2] - 17/02/2019 - -### Added - -- command `/filger help`. - -### Changed - -- api.lua removed. (useless) - -## [2.1] - 10/02/2019 - -### Added - -- Druid (all specs) spell list. -- Warrior (Protection) spell list. -- Shaman (Elemental and Restoration) spell list. -- Paladin (all specs) spell list. - -### Changed - -- Update MONK spell list. -- Test command duration error (*division by zero*). -- StatusBar was hiding timer text. -- Invalid spellID warnning receiving nil variable. - -## [2.0] - 09/02/2019 - -### Added - -- PRIEST spell table for Discipline, Holy and Shadow. -- MONK spell table for Brewmaster and Mistweaver. -- Instance/Zone Tables. -- import only current instance/zone bosses spell list. -- Cataclysm Raid spell list.. -- Mists of Pandaria Raids and Dungeon spell list. -- Commands /filger [show][hide][test]. - -### Changed - -- New implementation -- Update to support `patch 8.1` - **World of Warcraft - Battle for Azeroth**. - -## [1.5] - 04/07/2018 - -### Changed - -- Fixed some miss spelling. -- Update `README.md` - -### Added - -- Inserted racial skills (Troll and Orc) - -## [1.4] - 01/03/2018 - -### Changed - -- Update SHAMAN spells (Resurgence and Ancestral Vigor) - -## [1.3] - 27/02/2018 - -### Changed - -- Fixed `element.count` error when `data.count` got `nil` as value. -- Reviewd spell list. - -### Added - -- Insert PALADIN spells - -## [1.2] - 19/02/2018 - -### Changed - -- Update spell list. - -## [1.1] - 12/02/2018 - -### Added - -- Sorting active spell by expiration time else spellID. -- Command `/filger` -- Feature to record buffs/debuffs on player and target units. -- Saved variables for recorded buffs/debuffs. - -## [1.0] - 11/02/2018 - -### Added - -- Release. -- Spell list for PRIEST, MONK, DRUID and Shaman -- Icons border are colored by spell type. +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Removed + +- Unused libraries LibClassicDurations and CallbackHandler-1.0 + +## [5.3.0] - 2026-10-19 + +### Added + +- Support for Midnight (12.0) secret values: restricted auras are looked up by listed spell ids, secret cooldowns use duration objects +- Support for Classic Forever, TBC Anniversary and Titan WotLK clients +- Spells for Mists of Pandaria Classic +- CI: luacheck lint, interface version bump, packager release + +### Changed + +- Single `Filger.toc` for every client; removed `Filger_Classic.toc` and `Filger_Mists.toc` +- Spell data split into per-client `data/` folders, loaded through the TOC `[Game]` variable +- Replaced deprecated item/spell globals with `C_Item`, `C_Container`, `C_SpellBook` and `C_Spell` +- Debuff colors from `AuraUtil` +- Updated libraries (LibStub, CallbackHandler-1.0, LibDispel) + +### Removed + +- Cataclysm Classic from the TOC + +### Fixed + +- luacheck warnings, leaked `numVisible` and `itemType` globals + +## [5.2.1] - 2025-03-08 + +### Added + +- Support for classic era +- Spells updates + +### Changed + +- Update classic warrior cooldown +- Added classic elixirs to blacklist + +## [5.2.0] - 2024-10-29 + +### Added + +- Update shaman enhancement spells and cooldowns +- Added items spells priority + +### Changed + +- Update spell priority +- Update processing and filtering spells + +## [5.1.1] - 2024-10-07 + +### Changed + +- replace `_G.IsAddOnLoaded` with `C_AddOns.IsAddOnLoaded` (patch 11.0.2) + +## [5.1.0] - 2024-08-04 + +### Changed + +- Update spells for patch 11.0.0 +- Added cataclysm spells +- Fix cataclysm cooldowns +- Fix dispelable aura highlight + +## [5.0.0] - 2024-07-27 + +### Changed + +- Complete rework +- Update `UNIT_AURA` event to use `UnitAuraInfo` +- Update API to use `C_Spell` namespace. +- Added new lib `LibDispel`. +- Fixed aura colors and dispelable/stealable animation. + +## [4.3.2] - 2024-07-26 + +### Changed + +- Update .toc files +- Update submodules + +## [4.3.1] - 2024-01-13 + +### Changed + +- Adding `LibStub` as git submodule +- Adding `CallbackHandler-1.0` as git submodule +- Adding `LibClassicDurations` as git submodule + +## [4.3.0] - 2024-01-13 + +### Changed + +- Changed Tukui dependency from `required` to `optional` +- Fixed `CreateBackdrop` error, caused by required depencies + +## [4.2] - 2024-01-02 + +### Added + +- `CHANGELOG.mf' initialized. + +## [2.5] - 24/10/2019 + +### Changed + +- Patch 8.2.5 + +## [2.4] - 06/03/2019 + +### Added + +- WoLK dungeons and raid tables. +- Update BoA dungeon and raid tables. +- Tracer plugins (help to build spell tables by saving buffs/debuffs that affect player) + +### Changed + +- Tracer plugin enhanced (added automatic option). + +## [2.3] - 04/02/2019 + +### Changed + +- Fixed icon cooldown countdown double text. +- Fixed timer text for auras with no expiration time. + +## [2.2] - 17/02/2019 + +### Added + +- command `/filger help`. + +### Changed + +- api.lua removed. (useless) + +## [2.1] - 10/02/2019 + +### Added + +- Druid (all specs) spell list. +- Warrior (Protection) spell list. +- Shaman (Elemental and Restoration) spell list. +- Paladin (all specs) spell list. + +### Changed + +- Update MONK spell list. +- Test command duration error (*division by zero*). +- StatusBar was hiding timer text. +- Invalid spellID warnning receiving nil variable. + +## [2.0] - 09/02/2019 + +### Added + +- PRIEST spell table for Discipline, Holy and Shadow. +- MONK spell table for Brewmaster and Mistweaver. +- Instance/Zone Tables. +- import only current instance/zone bosses spell list. +- Cataclysm Raid spell list.. +- Mists of Pandaria Raids and Dungeon spell list. +- Commands /filger [show][hide][test]. + +### Changed + +- New implementation +- Update to support `patch 8.1` - **World of Warcraft - Battle for Azeroth**. + +## [1.5] - 04/07/2018 + +### Changed + +- Fixed some miss spelling. +- Update `README.md` + +### Added + +- Inserted racial skills (Troll and Orc) + +## [1.4] - 01/03/2018 + +### Changed + +- Update SHAMAN spells (Resurgence and Ancestral Vigor) + +## [1.3] - 27/02/2018 + +### Changed + +- Fixed `element.count` error when `data.count` got `nil` as value. +- Reviewd spell list. + +### Added + +- Insert PALADIN spells + +## [1.2] - 19/02/2018 + +### Changed + +- Update spell list. + +## [1.1] - 12/02/2018 + +### Added + +- Sorting active spell by expiration time else spellID. +- Command `/filger` +- Feature to record buffs/debuffs on player and target units. +- Saved variables for recorded buffs/debuffs. + +## [1.0] - 11/02/2018 + +### Added + +- Release. +- Spell list for PRIEST, MONK, DRUID and Shaman +- Icons border are colored by spell type. diff --git a/CLAUDE.md b/CLAUDE.md index b2dca78..713d196 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -65,6 +65,7 @@ I review and test changes in-game, then group them into commits myself. - Don't create, edit or comment on GitHub issues/PRs unless I explicitly ask. - Issue workflow: read the issue → inspect code → propose a plan → implement → leave uncommitted. - Never add `Co-authored by Claude Code` on commits, PRs or issues. +- Do not mention documentations, plans, any claude code md file in commits or PR. ## Verifying diff --git a/Filger.lua b/Filger.lua index cdd22d0..7205112 100644 --- a/Filger.lua +++ b/Filger.lua @@ -1,612 +1,44 @@ -local _, ns = ... -local Filger = ns.Filger - --- Blizzard -local CreateFrame = _G.CreateFrame -local IsSpellKnown = C_SpellBook.IsSpellKnown -local GetInventoryItemLink = _G.GetInventoryItemLink -local GetItemInfo = C_Item.GetItemInfo -local GetItemCooldown = C_Container.GetItemCooldown -local GetInventoryItemCooldown = _G.GetInventoryItemCooldown -local GetOverrideSpell = C_Spell and C_Spell.GetOverrideSpell -local C_Secrets = _G.C_Secrets -local issecretvalue = _G.issecretvalue -local C_StringUtil = _G.C_StringUtil -local C_CurveUtil = _G.C_CurveUtil -local C_DurationUtil = _G.C_DurationUtil -local Enum = _G.Enum - --------------------------------------------------- --- Filger --------------------------------------------------- -local isInit = false - --------------------------------------------------- --- BUTTONS --------------------------------------------------- -do - local button_proto = {} - - -- secret durations: text and color like OnUpdate ("2m", "45s", "4.3"; red under 5s) - local timeFormatter = C_StringUtil.CreateSecondsFormatter() - timeFormatter:SetDefaultAbbreviation(Enum.SecondsFormatterAbbreviation.OneLetter) - timeFormatter:SetStripIntervalWhitespace(Enum.SecondsFormatterIntervalWhitespace.StripIgnoreLocale) - timeFormatter:SetDesiredUnitCount(1) - timeFormatter:SetCanRoundUpLastUnit(true) - timeFormatter:SetMillisecondsThreshold(5) - - local timeColorCurve = C_CurveUtil.CreateColorCurve() - timeColorCurve:SetType(Enum.LuaCurveType.Step) - timeColorCurve:AddPoint(0, CreateColor(0.99, 0.31, 0.31)) - timeColorCurve:AddPoint(5, CreateColor(0.84, 0.75, 0.65)) - - -- aura container duration text (Mainline); Classic bindings have no SetTextColorCurve - if Filger.isMainline then - Filger.timeTextOptions = { - textFormatter = timeFormatter, - textColor = { curve = timeColorCurve, property = Enum.DurationTextBindingProperty.RemainingDuration } - } - end - - function button_proto:OnEnter() - if (GameTooltip:IsForbidden() and not self:IsVisible()) then return end - - GameTooltip:SetOwner(self, "ANCHOR_RIGHT") - - if self.auraInstanceID and GameTooltip.SetUnitDebuffByAuraInstanceID then - if self.isHarmful then - GameTooltip:SetUnitDebuffByAuraInstanceID(self.unit, self.auraInstanceID) - else - GameTooltip:SetUnitBuffByAuraInstanceID(self.unit, self.auraInstanceID) - end - elseif self.itemId then - GameTooltip:SetItemByID(self.itemId) - elseif self.slotId then - GameTooltip:SetInventoryItem(self.unit, self.slotId) - elseif self.spellId then - GameTooltip:SetSpellByID(self.spellId) - end - end - - -- secret durations can't drive the OnUpdate timer text, the duration text binding is used instead - function button_proto:SetSecret(isSecret) - if self.isSecret == isSecret then return end - self.isSecret = isSecret - if self.TimeBinding then - self.TimeBinding:SetEnabled(isSecret) - end - self.Time:SetText("") - if isSecret then - self:SetScript("OnUpdate", nil) - end - end - - function button_proto:OnLeave() - if GameTooltip:IsForbidden() then return end - GameTooltip:Hide() - end - - function button_proto:OnUpdate(elapsed) - self.elapsed = (self.elapsed or 0) + elapsed - if self.elapsed >= 0.1 then - local remaining = (self.expirationTime or 0) - GetTime() - if self.Time then - if remaining > 0 then - self.Time:SetText(Filger.FormatTime(remaining)) - if remaining <= 5 then - self.Time:SetTextColor(0.99, 0.31, 0.31) - else - self.Time:SetTextColor(0.84, 0.75, 0.65) - end - else - self.Time:SetText("") - self:SetScript("OnUpdate", nil) - self:Hide() - end - end - self.elapsed = 0 - end - end - - --[[ Filger:StyleButton(button) - Creates the button's visual regions (backdrop, cooldown, icon, count, time text). - Shared by Filger buttons and aura container buttons. - --]] - function Filger:StyleButton(button) - -- resources - local font, fontSize, fontStyle = Filger.fonts.normal, 12, "OUTLINE" - -- aura container buttons are unnamed; "$parent" would create globals like "Icon" - local name = button:GetName() - - button:CreateBackdrop() - - local cooldown = CreateFrame("Cooldown", name and name .. "CD", button, "CooldownFrameTemplate") - cooldown:SetAllPoints() - cooldown:SetReverse(true) - cooldown:SetDrawEdge(false) - cooldown:SetHideCountdownNumbers(true) - -- cooldown.noOCC = true - -- cooldown.noCooldownCount = true - button.Cooldown = cooldown - - local icon = button:CreateTexture(name and name .. "Icon", "ARTWORK") - icon:SetAllPoints() - icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) - button.Icon = icon - - local overlay = CreateFrame("Frame", name and name .. "Overlay", button, nil) - overlay:SetAllPoints(button) - overlay:SetFrameLevel(cooldown:GetFrameLevel() + 1) - button.Overlay = overlay - - local count = overlay:CreateFontString(name and name .. "OverlayCount", "OVERLAY") - count:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 0, 0) - count:SetFont(font, fontSize, fontStyle) - count:SetJustifyH("RIGHT") - count:SetTextColor(0.84, 0.75, 0.65) - button.Count = count - - local time = overlay:CreateFontString(name and name .. "OverlayTime", "OVERLAY") - time:SetPoint("CENTER", button, "CENTER", 0, 0) - time:SetFont(font, 20, fontStyle) - time:SetJustifyH("CENTER") - time:SetTextColor(0.84, 0.75, 0.65) - button.Time = time - end - - --[[ Callback: button_proto:CreateButton(element, index) - Creates an aura/cooldown button at a given position. - - * self - the widget holding the aura buttons - * index - the position at which the aura button is to be created (number) - - ## Returns - * button - the button used to represent the aura (Button) - --]] - function Filger:CreateButton(element, index) - local button = Mixin(CreateFrame("Frame", element:GetName() .. "Aura" .. index, element), button_proto) - button:SetWidth(element.size) - button:SetHeight(element.size) - self:StyleButton(button) - - local time = button.Time - - -- only secret cooldowns (Mainline) need it; Classic bindings have no SetTextColorCurve - if Filger.isMainline then - local timeBinding = C_DurationUtil.CreateDurationTextBinding() - timeBinding:SetFontString(time) - timeBinding:SetFormatter(timeFormatter) - timeBinding:SetTextColorCurve(timeColorCurve, Enum.DurationTextBindingProperty.RemainingDuration) - timeBinding:SetExpiredText("") - timeBinding:SetZeroDurationText("") - timeBinding:SetEnabled(false) - button.TimeBinding = timeBinding - end - - local animation = button:CreateAnimationGroup() - animation:SetLooping("BOUNCE") - button.Animation = animation - - local fadeout = animation:CreateAnimation("Alpha") - fadeout:SetFromAlpha(1) - fadeout:SetToAlpha(0.10) - fadeout:SetDuration(0.50) - fadeout:SetSmoothing("IN_OUT") - animation.Fadeout = fadeout - - if self.config.general.gametooltip then - button:SetScript("OnEnter", button.OnEnter) - button:SetScript("OnLeave", button.OnLeave) - end - - return button - end -end - --------------------------------------------------- --- COOLDOWNS --------------------------------------------------- -local cooldown_proto = { - casted = {}, - spells = {}, - createdButtons = 0, - anchoredButtons = 0, - visibleButtons = 0 -} - -do - --[[ Callback: cooldown_proto:Update(unit, data, index) - Update the cooldown button and its components. - - * self - the widget holding the aura buttons - * unit - the unit on which the aura is cast (string) - * data - the cooldown object (table) - * index - the actual position of the aura button (number) - ]]-- - function cooldown_proto:Update(unit, data, index) - if not data.name then return end - - local button = self[index] - if not button then - button = Filger:CreateButton(self, index) - - table.insert(self, button) - - self.createdButtons = self.createdButtons + 1 - end - - button:SetSecret(data.durationObject ~= nil) - - -- tooltips - button.unit = unit - button.spellId = data.spellId - button.slotId = data.slotId - button.itemId = data.itemId - - -- timers - button.name = data.name - -- button.spellId = data.spellId - button.duration = data.duration or 0 - button.expirationTime = data.expirationTime or GetTime() - - if button.Cooldown then - if data.durationObject then - button.Cooldown:SetCooldownFromDurationObject(data.durationObject) - button.TimeBinding:SetDuration(data.durationObject) - button.Cooldown:Show() - elseif data.duration and data.duration > 0 then - button.Cooldown:SetCooldown(data.expirationTime - data.duration, data.duration, data.timeMod) - button.Cooldown:Show() - else - button.Cooldown:Hide() - end - end - - if button.Icon then - if data.icon then - button.Icon:SetTexture(data.icon) - button.Icon:Show() - else - button.Icon:Hide() - end - end - - if data.duration and data.duration > 0 then - button:SetScript("OnUpdate", button.OnUpdate) - end - - button:Show() - end - - --[[ Override: cooldown_proto:FilterCooldown(data) - Defines a custom filter that controls if the cooldown button should be shown. - - * self - the widget holding the aura buttons - * data - the cooldown object (table) - - ## Returns - - * show - indicates whether the aura button should be shown (boolean) - --]] - function cooldown_proto:FilterCooldown(data) - if not data.name then return end - -- secret cooldown: already known to be active and off GCD; casts can't be recorded while restricted - if data.durationObject then - return data.enabled and data.isKnown - end - return data.name - and data.enabled - and data.isKnown - and data.casted - and (data.duration and data.duration > 1.5) -- classic sometimes bring 1.5 duration - and (data.start and data.start > 0) - end - - --[[ Override: cooldown_proto.SortCooldowns(a, b) - Defines a custom sorting algorithm for ordering the cooldowns. - - Overridden by the more specific SortCooldowns overrides if they are defined. - --]] - cooldown_proto.SortCooldowns = function(a, b) - if (a.expirationTime ~= b.expirationTime) then - return a.expirationTime < b.expirationTime - end - return a.index < b.index - end - - function cooldown_proto:ProcessCooldownData(unit, data) - local index, name, icon, start, duration, enabled, durationObject - - if data.spellId then - index = "SPELL_" .. data.spellId - local info = Filger.GetSpellInfo(data.spellId) - if info then - name = info.name - icon = info.iconID - end - local cdInfo = Filger.GetSpellCooldown(data.spellId) - if cdInfo then - if C_Secrets and C_Secrets.ShouldSpellCooldownBeSecret(data.spellId) then - -- startTime/duration are secret; isActive/isOnGCD never are - if cdInfo.isActive and not cdInfo.isOnGCD then - durationObject = C_Spell.GetSpellCooldownDuration(data.spellId) - end - else - start = cdInfo.startTime - duration = cdInfo.duration - end - enabled = cdInfo.isEnabled and 1 or 0 - end - elseif data.slotId then - index = "SLOT_" .. data.slotId - local itemLink = GetInventoryItemLink(unit, data.slotId) - if (itemLink) then - name, _, _, _, _, _, _, _, _, icon, _, _, _, _, _, _, _ = GetItemInfo(itemLink) - start, duration, enabled = GetInventoryItemCooldown(unit, data.slotId) - else - enabled = 0 - end - elseif data.itemId then - index = "ITEM_" .. data.itemId - name, _, _, _, _, _, _, _, _, icon, _, _, _, _, _, _, _ = GetItemInfo(data.itemId) - start, duration, enabled = GetItemCooldown(data.itemId) - end - - duration = duration or 0 - start = start or 0 - - return { - index = index, - name = name, - spellId = data.spellId, - -- a talent can replace one tracked spell with another (e.g. Sentinel overrides Avenging Wrath) - activeSpellId = data.spellId and GetOverrideSpell and GetOverrideSpell(data.spellId) or data.spellId, - slotId = data.slotId, - itemId = data.itemId, - icon = icon, - start = start, - duration = duration, - expirationTime = start + duration, - durationObject = durationObject, - enabled = (enabled ~= 0), - casted = (not data.spellId) and true or self.casted[data.spellId], - isKnown = (not data.spellId) and true or IsSpellKnown(data.spellId) - } - end - - function cooldown_proto:UpdateCooldowns(event, unit, spellID, baseSpellID) - local changed = true - self.active = self.active or {} - if not spellID then - table.wipe(self.active) - end - - for _, row in next, self.spells do - -- no event fires when a cooldown ends, so shown rows are re-checked on every event; - -- slots and items are always re-checked because their events carry the item's spell ID - if not spellID or not row.spellId or self.active[row] - or row.spellId == spellID or row.spellId == baseSpellID then - local data = self:ProcessCooldownData(unit, row) - - if self:FilterCooldown(data) then - self.active[row] = data - else - self.active[row] = nil - if data.spellId then - self.casted[data.spellId] = nil - end - end - end - end - - if changed then - -- instead of removing auras one by one, just wipe the tables entirely - -- and repopulate them, multiple table.remove calls are insanely slow - self.sorted = table.wipe(self.sorted or {}) - self.shownSpells = table.wipe(self.shownSpells or {}) - - for _, data in next, self.active do - local id = data.activeSpellId - if not id or not self.shownSpells[id] then - if id then self.shownSpells[id] = true end - table.insert(self.sorted, data) - end - end - - table.sort(self.sorted, self.SortCooldowns) - - local numVisible = math.min(self.limit, #self.sorted) - - for i = 1, numVisible do - self:Update(unit, self.sorted[i], i) - end - - local visibleChanged = true - - if (numVisible ~= self.visibleButtons) then - self.visibleButtons = numVisible - visibleChanged = true - end - - for i = numVisible + 1, #self do - self[i]:Hide() - end - - if (visibleChanged or self.createdButtons > self.anchoredButtons) then - if visibleChanged then - self:SetPosition(1, numVisible) - else - self:SetPosition(self.anchoredButtons + 1, self.createdButtons) - self.anchoredButtons = self.createdButtons - end - end - end - end - - function cooldown_proto:ForceUpdate() - self:UpdateCooldowns("ForceUpdate", self.unit, nil) - end - - -- spellID is nil when every cooldown should be updated - function cooldown_proto:SPELL_UPDATE_COOLDOWN(spellID, baseSpellID) - self:UpdateCooldowns("SPELL_UPDATE_COOLDOWN", self.unit, spellID, baseSpellID) - end - - -- register which spell the player cast - function cooldown_proto:UNIT_SPELLCAST_SUCCEEDED(unit, guid, spellId) - if issecretvalue and issecretvalue(spellId) then return end - self.casted[spellId] = true - end -end - --------------------------------------------------- --- FILGER --------------------------------------------------- - -do - local element_proto = {} - - function element_proto:SetPosition(from, to) - local element = self - local sizex = element.size + element.spacing - local sizey = element.size + element.spacing - local anchor = element.initialAnchor or "BOTTOMLEFT" - local growthx = (element["growth-x"] == "LEFT" and -1) or 1 - local growthy = (element["growth-y"] == "DOWN" and -1) or 1 - local cols = math.floor(element:GetWidth() / sizex + 0.5) - - for i = from, to do - local button = element[i] - if (not button) then break end - - if anchor == "CENTER" then - -- Centralized positioning logic - local index = i - from - local col, row - if index % 2 == 0 then - col = math.floor(index / 2) - else - col = -math.floor(index / 2) - 1 - end - row = math.floor(index / cols) - - button:ClearAllPoints() - button:SetPoint(anchor, element, anchor, col * sizex, row * sizey) - else - local col = (i - 1) % cols - local row = math.floor((i - 1) / cols) - - button:ClearAllPoints() - button:SetPoint(anchor, element, anchor, col * sizex * growthx, row * sizey * growthy) - end - end - end - - function Filger:Spawn(key, data) - -- "PLAYER_AURAS" -> "FilgerPlayerAuras" - local name = key:lower():gsub("^%l", string.upper):gsub("_(%l)", string.upper) - local element = Mixin(CreateFrame("Frame", self:GetName() .. name, self), element_proto) - - -- configuration - element.unit = data.unit or "player" - element.filter = data.filter or "HELPFUL" - element.caster = data.caster or nil - - element.limit = data.limit or 32 - element.size = data.size or 32 - element.spacing = data.spacing or 5 - element.initialAnchor = data.initialAnchor or "BOTTOMLEFT" - element["growth-x"] = data["growth-x"] or "LEFT" - element["growth-y"] = data["growth-y"] or "DOWN" - - element:SetPoint(unpack(data.anchor)) - element:SetWidth((element.limit * element.size) + (element.limit - 1) * element.spacing) - element:SetHeight(element.size) - -- element:CreateBackdrop() - - -- local text = element:CreateFontString(nil, "OVERLAY") - -- text:SetPoint("CENTER", element, "CENTER", 0, 1) - -- text:SetFont(Font, FontSize, FontStyle) - -- text:SetText(element:GetName()) - -- text:SetJustifyH("CENTER") - -- text:Hide() - -- element.text = text - - if (element.filter == "COOLDOWN") then - element = Mixin(element, cooldown_proto) - - -- overrider element functions with configurations - element.SortCooldowns = data.SortCooldowns or element.SortCooldowns - element.FilterCooldown = data.FilterCooldown or element.FilterCooldown - - element:RegisterEvent("SPELL_UPDATE_COOLDOWN") - element:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", self.unit) - elseif data.groups then - -- Mainline only: Blizzard's secure aura container filters and displays the auras - element = Mixin(element, ns.container_proto) - element:CreateContainer(data) - else - element = Mixin(element, ns.aura_proto) - - -- overrider element functions with configurations - element.SortAuras = data.SortAuras or element.SortAuras - element.FilterAura = data.FilterAura or element.FilterAura - - element:RegisterEvent("UNIT_AURA") - - if (element.unit == "target") then - element:RegisterEvent("PLAYER_TARGET_CHANGED") - end - - if (element.unit == "focus") then - element:RegisterEvent("PLAYER_FOCUS_CHANGED") - end - end - - element.spells = data.spells or element.spells or {} - - element:SetScript("OnEvent", function(self, event, ...) - -- call event handler - self[event](self, ...) - end) - - return element - end -end - -Filger:RegisterEvent("ADDON_LOADED") -Filger:RegisterEvent("PLAYER_LOGIN") -Filger:SetScript("OnEvent", function(self, event, ...) - if self[event] then - -- call event handler - self[event](self, ...) - end -end) - -function Filger:ADDON_LOADED(addon) - if (addon ~= "Filger") then return end - self.unit = "player" - self.guid = UnitGUID(self.unit) - - -- Filger standalone - if self.MergeAPI then - self:MergeAPI() - end -end - -function Filger:PLAYER_LOGIN() - if not isInit then - -- self:RegisterEvent("PLAYER_ENTERING_WORLD") - - -- create panels based on spell list - for key, data in pairs(self.config.frames) do - if (data.enabled) then - local element = self:Spawn(key, data) - element:ForceUpdate() - table.insert(self, element) - end - end - - isInit = true - end -end +local _, ns = ... +local Filger = ns.Filger + +-------------------------------------------------- +-- Filger +-------------------------------------------------- +local isInit = false + +Filger:RegisterEvent("ADDON_LOADED") +Filger:RegisterEvent("PLAYER_LOGIN") +Filger:SetScript("OnEvent", function(self, event, ...) + if self[event] then + -- call event handler + self[event](self, ...) + end +end) + +function Filger:ADDON_LOADED(addon) + if (addon ~= "Filger") then return end + self.unit = "player" + self.guid = UnitGUID(self.unit) + + -- Filger standalone + if self.MergeAPI then + self:MergeAPI() + end +end + +function Filger:PLAYER_LOGIN() + if not isInit then + -- self:RegisterEvent("PLAYER_ENTERING_WORLD") + + -- create panels based on spell list + for key, data in pairs(self.config.frames) do + if (data.enabled) then + local element = self:Spawn(key, data) + element:ForceUpdate() + table.insert(self, element) + end + end + + isInit = true + end +end diff --git a/Filger.toc b/Filger.toc index 8d32ae1..6880a79 100644 --- a/Filger.toc +++ b/Filger.toc @@ -1,27 +1,34 @@ -## Interface: 11509, 20506, 38002, 50504, 120105, 16001 -## Version: 5.3.0 -## Author: Luaerror -## Credits: Nils Ruesch, Shestak, Affli, Garagar, hidekki, FourOne, Tukz - -## OptionalDeps: Tainted, Tukui -## SavedVariables: FilgerData - -## Notes: Minimal buff/debuff tracker -## Title: |cffff8000Filger|r - -## IconTexture: Interface\ICONS\Spell_Shadow_Shadowwordpain - -## X-Website: https://github.com/wow-lua/Filger -## X-Maintainer: pedrozc90 - -libs\init.xml -core\data.lua -data\[Game]\blacklist.lua -data\[Game]\cooldowns.lua -data\[Game]\spells.lua -data\shared\blacklist.lua -data\shared\cooldowns.lua -core\init.xml -config.lua -core\[Family]\auras.lua -Filger.lua +## Interface: 11509, 20506, 38002, 50504, 120105, 16001 +## Version: 5.3.0 +## Author: Luaerror +## Credits: Nils Ruesch, Shestak, Affli, Garagar, hidekki, FourOne, Tukz + +## OptionalDeps: Tainted, Tukui, ElvUI +## SavedVariables: FilgerData + +## Notes: Minimal buff/debuff tracker +## Title: |cffff8000Filger|r + +## IconTexture: Interface\ICONS\Spell_Shadow_Shadowwordpain + +## X-Website: https://github.com/wow-lua/Filger +## X-Maintainer: pedrozc90 + +libs\init.xml +# data plumbing, then data files push rows into ns.data +core\data.lua +data\[Game]\blacklist.lua +data\[Game]\cooldowns.lua +data\[Game]\spells.lua +data\shared\blacklist.lua +data\shared\cooldowns.lua +# Filger frame, client flags, utils, api, button, element, commands +core\init.xml +# validates data (Finalize), builds Filger.config (needs core + data) +config.lua +# cooldown element +elements\cooldowns.lua +# aura prototype; adds what each aura frame shows to Filger.config.frames +elements\[Family]\auras.lua +# spawns frames at PLAYER_LOGIN +Filger.lua diff --git a/LICENSE b/LICENSE index b7c9334..01b30c0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,19 +1,19 @@ -Copyright (c) 2019-2025 Luaerror - -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. +Copyright (c) 2019-2025 Luaerror + +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. diff --git a/config.lua b/config.lua index ff902aa..b931ae2 100644 --- a/config.lua +++ b/config.lua @@ -8,22 +8,17 @@ local cooldowns = Filger.cooldowns or {} ------------------------------------------------------------ -- Config ------------------------------------------------------------ ---[[ - * filter -- list of filters, separated by spaces or pipes. "HELPFUL" by default. - -- "HELPFUL" : Buffs - -- "HARMFUL" : Debuffs - -- "PLAYER" : Auras Debuffs applied by the player - -- "RAID" : Buffs the player can apply and debuffs the player can dispell - -- "CANCELABLE" : Buffs that can be cancelled with /cancelaura or CancelUnitBuff() - -- "NOT_CANCELABLE" : Buffs that cannot be cancelled - -- "INCLUDE_NAME_PLATE_ONLY" : Auras that should be shown on nameplates - -- "MAW" : Torghast Anima Powers - * unit -- unit whose auras to query. ("player", "target", "focus", etc.) - * caster -- aura caster. - * showOnlyPlayer -- - * isDebuff -- - * hidePlayer -- -]] + +-- aura frame layout; elements//auras.lua adds what each frame shows +local size, spacing = 35, 5 +local xOffset, yOffset = 239, 39 + +local left = { limit = 8, size = size, spacing = spacing, initialAnchor = "BOTTOMRIGHT", ["growth-x"] = "LEFT", ["growth-y"] = "DOWN" } +local right = { limit = 8, size = size, spacing = spacing, initialAnchor = "BOTTOMLEFT", ["growth-x"] = "RIGHT", ["growth-y"] = "DOWN" } + +local function Frame(defaults, data) + return Mixin({ enabled = true }, defaults, data) +end Filger.config = { ["general"] = { @@ -34,22 +29,13 @@ Filger.config = { ["color"] = Filger:CreateColor(0.00, 0.00, 0.00), ["size"] = 1 }, - ["well_fed"] = false, -- enabled filter to hide well fed buff (too many spells with this name) ["gametooltip"] = true, -- enabled tooltips }, - -- shared by the aura frames in core//auras.lua - ["layout"] = { - size = 35, - spacing = 5, - xOffset = 239, - yOffset = 39 - }, ["frames"] = { ["PLAYER_COOLDOWNS"] = { enabled = true, + type = "cooldowns", unit = "player", - filter = "COOLDOWN", - caster = "player", anchor = { "CENTER", UIParent, "BOTTOM", 0, 175 }, limit = 8, size = 38, @@ -59,6 +45,16 @@ Filger.config = { ["growth-x"] = "CENTER", ["growth-y"] = "CENTER", spells = cooldowns - } + }, + -- LEFT + ["PLAYER_DEBUFFS"] = Frame(left, { unit = "player", anchor = { "RIGHT", UIParent, "CENTER", -xOffset, yOffset } }), + ["PLAYER_AURAS"] = Frame(left, { unit = "player", anchor = { "RIGHT", UIParent, "CENTER", -xOffset, 0 } }), + ["PLAYER_BUFFS"] = Frame(left, { unit = "player", anchor = { "RIGHT", UIParent, "CENTER", -xOffset, -yOffset } }), + -- RIGHT + ["TARGET_DEBUFFS"] = Frame(right, { unit = "target", anchor = { "LEFT", UIParent, "CENTER", xOffset, yOffset } }), + ["TARGET_AURAS"] = Frame(right, { unit = "target", anchor = { "LEFT", UIParent, "CENTER", xOffset, 0 } }), + ["FOCUS_DEBUFFS"] = Frame(right, { unit = "focus", anchor = { "LEFT", UIParent, "CENTER", xOffset, -yOffset } }), + -- CENTER + ["BOSS_DEBUFFS"] = Frame(right, { unit = "player", anchor = { "CENTER", UIParent, "CENTER", 0, 160 }, limit = 1, size = math.floor(1.5 * size) }) } } diff --git a/core/api.lua b/core/api.lua index dcbd363..973c84b 100644 --- a/core/api.lua +++ b/core/api.lua @@ -1,126 +1,107 @@ -local _, ns = ... -local Filger = ns.Filger -local textures = Filger.textures - --- Blizzard -local IsAddOnLoaded = C_AddOns and C_AddOns.IsAddOnLoaded or _G.IsAddOnLoaded - --- skip it if Tukui exists -if IsAddOnLoaded("Tainted") or IsAddOnLoaded("Tukui") then - return -end - ---------------------------------------------------- --- API ---------------------------------------------------- -local API = {} - -local Scale = function(size) - -- Ensure 'size' is a valid number, default to 1 if not - size = tonumber(size) or 1 - - -- Retrieve the UI scale value and ensure it is valid - -- Default to 1 if 'uiScale' is not a valid number - local uiScale = tonumber(GetCVar("uiScale")) - if not uiScale then - uiScale = 1 - end - - -- Calculate the scaling multiplier - local mult = Filger.pixelPerfectScale / uiScale - - -- Return the scaled size, rounded to the nearest integer - return mult * math.floor(size / mult + 0.5) -end - -API.SetOutside = function(self, anchor, xOffset, yOffset) - xOffset = xOffset and Scale(xOffset) or Scale(1) - yOffset = yOffset and Scale(yOffset) or Scale(1) - - anchor = anchor or self:GetParent() - - if self:GetPoint() then - self:ClearAllPoints() - end - - self:SetPoint("TOPLEFT", anchor, "TOPLEFT", -xOffset, yOffset) - self:SetPoint("BOTTOMRIGHT", anchor, "BOTTOMRIGHT", xOffset, -yOffset) -end - -API.SetInside = function(self, anchor, xOffset, yOffset) - xOffset = xOffset and Scale(xOffset) or Scale(1) - yOffset = yOffset and Scale(yOffset) or Scale(1) - - anchor = anchor or self:GetParent() - - if self:GetPoint() then - self:ClearAllPoints() - end - - self:SetPoint("TOPLEFT", anchor, "TOPLEFT", xOffset, -yOffset) - self:SetPoint("BOTTOMRIGHT", anchor, "BOTTOMRIGHT", -xOffset, yOffset) -end - -API.CreateBackdrop = function(self, template) - if not self.Backdrop then - local backdropTexture = textures.blank - local backdropColor = Filger.config.general.backdrop.color - local backdropAlpha = (template == "transparent") and 0.70 or 1 - - local borderTexture = textures.blank - local borderColor = Filger.config.general.border.color - - local inset = Scale(Filger.config.general.border.size or 1) - local backdrop = { - bgFile = backdropTexture - } - - if (template ~= "solid") then - backdrop.edgeFile = borderTexture - backdrop.edgeSize = inset - backdrop.insets = { top = inset, left = inset, bottom = inset, right = inset } - end - - local level = self:GetFrameLevel() or 1 - - self.Backdrop = CreateFrame("Frame", nil, self, "BackdropTemplate") - self.Backdrop:SetPoint("TOPLEFT", -inset, inset) - self.Backdrop:SetPoint("BOTTOMRIGHT", inset, -inset) - self.Backdrop:SetFrameLevel(level - 1) - self.Backdrop:SetBackdrop(backdrop) - self.Backdrop:SetBackdropColor(backdropColor.r, backdropColor.g, backdropColor.b, backdropAlpha) - self.Backdrop:SetBackdropBorderColor(borderColor.r, borderColor.g, borderColor.b) - end -end - --------------------------------------------------- --- Merge Filger API with WoW API --------------------------------------------------- -function Filger:MergeAPI() - local AddAPI = function(obj) - local mt = getmetatable(obj).__index - for name, func in pairs(API) do - if (not obj[name]) then mt[name] = func end - end - end - - local Handled = { - ["Frame"] = true - } - - local Object = CreateFrame("Frame") - AddAPI(Object) - AddAPI(Object:CreateTexture()) - AddAPI(Object:CreateFontString()) - - Object = EnumerateFrames() - - while (Object) do - local t = Object:GetObjectType() - if (not Object:IsForbidden() and not Handled[t]) then - AddAPI(Object) - Handled[t] = true - end - Object = EnumerateFrames(Object) - end -end +local _, ns = ... +local Filger = ns.Filger +local textures = Filger.textures + +-- Blizzard +local IsAddOnLoaded = C_AddOns and C_AddOns.IsAddOnLoaded or _G.IsAddOnLoaded + +-- Tukui borders are textures colored by Backdrop:SetBorderColor; ElvUI and Filger use a BackdropTemplate border +function Filger:SetBorderColor(backdrop, color) + if backdrop.SetBorderColor then + backdrop:SetBorderColor(color.r, color.g, color.b, color.a or 1) + else + backdrop:SetBackdropBorderColor(color.r, color.g, color.b, color.a or 1) + end +end + +-- skip it if a UI with its own CreateBackdrop is loaded +if IsAddOnLoaded("Tainted") or IsAddOnLoaded("Tukui") or IsAddOnLoaded("ElvUI") then + return +end + +--------------------------------------------------- +-- API +--------------------------------------------------- +local API = {} + +local Scale = function(size) + -- Ensure 'size' is a valid number, default to 1 if not + size = tonumber(size) or 1 + + -- Retrieve the UI scale value and ensure it is valid + -- Default to 1 if 'uiScale' is not a valid number + local uiScale = tonumber(GetCVar("uiScale")) + if not uiScale then + uiScale = 1 + end + + -- Calculate the scaling multiplier + local mult = Filger.pixelPerfectScale / uiScale + + -- Return the scaled size, rounded to the nearest integer + return mult * math.floor(size / mult + 0.5) +end + +API.CreateBackdrop = function(self, template) + if not self.Backdrop then + local backdropTexture = textures.blank + local backdropColor = Filger.config.general.backdrop.color + local backdropAlpha = (template == "transparent") and 0.70 or 1 + + local borderTexture = textures.blank + local borderColor = Filger.config.general.border.color + + local inset = Scale(Filger.config.general.border.size or 1) + local backdrop = { + bgFile = backdropTexture + } + + if (template ~= "solid") then + backdrop.edgeFile = borderTexture + backdrop.edgeSize = inset + backdrop.insets = { top = inset, left = inset, bottom = inset, right = inset } + end + + local level = self:GetFrameLevel() or 1 + + self.Backdrop = CreateFrame("Frame", nil, self, "BackdropTemplate") + self.Backdrop:SetPoint("TOPLEFT", -inset, inset) + self.Backdrop:SetPoint("BOTTOMRIGHT", inset, -inset) + self.Backdrop:SetFrameLevel(level - 1) + self.Backdrop:SetBackdrop(backdrop) + self.Backdrop:SetBackdropColor(backdropColor.r, backdropColor.g, backdropColor.b, backdropAlpha) + self.Backdrop:SetBackdropBorderColor(borderColor.r, borderColor.g, borderColor.b) + end +end + +-------------------------------------------------- +-- Merge Filger API with WoW API +-------------------------------------------------- +function Filger:MergeAPI() + local AddAPI = function(obj) + local mt = getmetatable(obj).__index + for name, func in pairs(API) do + if (not obj[name]) then mt[name] = func end + end + end + + local Handled = { + ["Frame"] = true + } + + local Object = CreateFrame("Frame") + AddAPI(Object) + AddAPI(Object:CreateTexture()) + AddAPI(Object:CreateFontString()) + + Object = EnumerateFrames() + + while (Object) do + local t = Object:GetObjectType() + if (not Object:IsForbidden() and not Handled[t]) then + AddAPI(Object) + Handled[t] = true + end + Object = EnumerateFrames(Object) + end +end diff --git a/core/button.lua b/core/button.lua new file mode 100644 index 0000000..6bd4b3e --- /dev/null +++ b/core/button.lua @@ -0,0 +1,193 @@ +local _, ns = ... +local Filger = ns.Filger + +-- Blizzard +local CreateFrame = _G.CreateFrame +local C_StringUtil = _G.C_StringUtil +local C_CurveUtil = _G.C_CurveUtil +local C_DurationUtil = _G.C_DurationUtil +local Enum = _G.Enum + +-------------------------------------------------- +-- BUTTONS +-------------------------------------------------- +do + local button_proto = {} + + -- secret durations: text and color like OnUpdate ("2m", "45s", "4.3"; red under 5s) + local timeFormatter = C_StringUtil.CreateSecondsFormatter() + timeFormatter:SetDefaultAbbreviation(Enum.SecondsFormatterAbbreviation.OneLetter) + timeFormatter:SetStripIntervalWhitespace(Enum.SecondsFormatterIntervalWhitespace.StripIgnoreLocale) + timeFormatter:SetDesiredUnitCount(1) + timeFormatter:SetCanRoundUpLastUnit(true) + timeFormatter:SetMillisecondsThreshold(5) + + local timeColorCurve = C_CurveUtil.CreateColorCurve() + timeColorCurve:SetType(Enum.LuaCurveType.Step) + timeColorCurve:AddPoint(0, CreateColor(0.99, 0.31, 0.31)) + timeColorCurve:AddPoint(5, CreateColor(0.84, 0.75, 0.65)) + + -- aura container duration text (Mainline); Classic bindings have no SetTextColorCurve + if Filger.isMainline then + Filger.timeTextOptions = { + textFormatter = timeFormatter, + textColor = { curve = timeColorCurve, property = Enum.DurationTextBindingProperty.RemainingDuration } + } + end + + function button_proto:OnEnter() + if (GameTooltip:IsForbidden() and not self:IsVisible()) then return end + + GameTooltip:SetOwner(self, "ANCHOR_RIGHT") + + if self.auraInstanceID and GameTooltip.SetUnitDebuffByAuraInstanceID then + if self.isHarmful then + GameTooltip:SetUnitDebuffByAuraInstanceID(self.unit, self.auraInstanceID) + else + GameTooltip:SetUnitBuffByAuraInstanceID(self.unit, self.auraInstanceID) + end + elseif self.itemId then + GameTooltip:SetItemByID(self.itemId) + elseif self.slotId then + GameTooltip:SetInventoryItem(self.unit, self.slotId) + elseif self.spellId then + GameTooltip:SetSpellByID(self.spellId) + end + end + + -- secret durations can't drive the OnUpdate timer text, the duration text binding is used instead + function button_proto:SetSecret(isSecret) + if self.isSecret == isSecret then return end + self.isSecret = isSecret + if self.TimeBinding then + self.TimeBinding:SetEnabled(isSecret) + end + self.Time:SetText("") + if isSecret then + self:SetScript("OnUpdate", nil) + end + end + + function button_proto:OnLeave() + if GameTooltip:IsForbidden() then return end + GameTooltip:Hide() + end + + function button_proto:OnUpdate(elapsed) + self.elapsed = (self.elapsed or 0) + elapsed + if self.elapsed >= 0.1 then + local remaining = (self.expirationTime or 0) - GetTime() + if self.Time then + if remaining > 0 then + self.Time:SetText(Filger.FormatTime(remaining)) + if remaining <= 5 then + self.Time:SetTextColor(0.99, 0.31, 0.31) + else + self.Time:SetTextColor(0.84, 0.75, 0.65) + end + else + self.Time:SetText("") + self:SetScript("OnUpdate", nil) + self:Hide() + end + end + self.elapsed = 0 + end + end + + --[[ Filger:StyleButton(button) + Creates the button's visual regions (backdrop, cooldown, icon, count, time text). + Shared by Filger buttons and aura container buttons. + --]] + function Filger:StyleButton(button) + -- resources + local font, fontSize, fontStyle = Filger.fonts.normal, 12, "OUTLINE" + -- aura container buttons are unnamed; "$parent" would create globals like "Icon" + local name = button:GetName() + + button:CreateBackdrop() + -- ElvUI stores its backdrop as `backdrop` + button.Backdrop = button.Backdrop or button.backdrop + + local cooldown = CreateFrame("Cooldown", name and name .. "CD", button, "CooldownFrameTemplate") + cooldown:SetAllPoints() + cooldown:SetReverse(true) + cooldown:SetDrawEdge(false) + cooldown:SetHideCountdownNumbers(true) + -- cooldown.noOCC = true + -- cooldown.noCooldownCount = true + button.Cooldown = cooldown + + local icon = button:CreateTexture(name and name .. "Icon", "ARTWORK") + icon:SetAllPoints() + icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + button.Icon = icon + + local overlay = CreateFrame("Frame", name and name .. "Overlay", button, nil) + overlay:SetAllPoints(button) + overlay:SetFrameLevel(cooldown:GetFrameLevel() + 1) + button.Overlay = overlay + + local count = overlay:CreateFontString(name and name .. "OverlayCount", "OVERLAY") + count:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 0, 0) + count:SetFont(font, fontSize, fontStyle) + count:SetJustifyH("RIGHT") + count:SetTextColor(0.84, 0.75, 0.65) + button.Count = count + + local time = overlay:CreateFontString(name and name .. "OverlayTime", "OVERLAY") + time:SetPoint("CENTER", button, "CENTER", 0, 0) + time:SetFont(font, 20, fontStyle) + time:SetJustifyH("CENTER") + time:SetTextColor(0.84, 0.75, 0.65) + button.Time = time + end + + --[[ Callback: button_proto:CreateButton(element, index) + Creates an aura/cooldown button at a given position. + + * self - the widget holding the aura buttons + * index - the position at which the aura button is to be created (number) + + ## Returns + * button - the button used to represent the aura (Button) + --]] + function Filger:CreateButton(element, index) + local button = Mixin(CreateFrame("Frame", element:GetName() .. "Aura" .. index, element), button_proto) + button:SetWidth(element.size) + button:SetHeight(element.size) + self:StyleButton(button) + + local time = button.Time + + -- only secret cooldowns (Mainline) need it; Classic bindings have no SetTextColorCurve + if Filger.isMainline then + local timeBinding = C_DurationUtil.CreateDurationTextBinding() + timeBinding:SetFontString(time) + timeBinding:SetFormatter(timeFormatter) + timeBinding:SetTextColorCurve(timeColorCurve, Enum.DurationTextBindingProperty.RemainingDuration) + timeBinding:SetExpiredText("") + timeBinding:SetZeroDurationText("") + timeBinding:SetEnabled(false) + button.TimeBinding = timeBinding + end + + local animation = button:CreateAnimationGroup() + animation:SetLooping("BOUNCE") + button.Animation = animation + + local fadeout = animation:CreateAnimation("Alpha") + fadeout:SetFromAlpha(1) + fadeout:SetToAlpha(0.10) + fadeout:SetDuration(0.50) + fadeout:SetSmoothing("IN_OUT") + animation.Fadeout = fadeout + + if self.config.general.gametooltip then + button:SetScript("OnEnter", button.OnEnter) + button:SetScript("OnLeave", button.OnLeave) + end + + return button + end +end diff --git a/core/colors.lua b/core/colors.lua index 35fb124..2dcb9b7 100644 --- a/core/colors.lua +++ b/core/colors.lua @@ -1,53 +1,53 @@ -local _, ns = ... -local Filger = ns.Filger - --- Blizzard -local ColorMixin = _G.ColorMixin - -local color_proto = { - SetRGBA = function(self, r, g, b, a) - if (r > 1 or g > 1 or b > 1) then - r, g, b = r / 255, g / 255, b / 255 - end - - self.r = r - self[1] = self.r - self.g = g - self[2] = self.g - self.b = b - self[3] = self.b - self.a = a or 1 - - -- pre-generate the hex color, there's no point to this being generated on the fly - self.hex = ("ff%.2x%.2x%.2x"):format(self:GetRGBAsBytes()); - end, - SetAtlas = function(self, atlas) - self.atlas = atlas - end, - GetAtlas = function(self) - return self.atlas - end, - GenerateHexColor = function(self) - return self.hex - end -} - -function Filger:CreateColor(r, g, b, a) - local color = Mixin({}, ColorMixin, color_proto) - color:SetRGBA(r, g, b, a) - return color -end - -local colors = { - ["Stealable"] = Filger:CreateColor(0.93, 0.91, 0.55), - ["Bleed"] = Filger:CreateColor(0.90, 0.10, 0.10), - ["Enrage"] = Filger:CreateColor(0.78, 0.25, 0.25) -} - -for debuffType, info in next, AuraUtil.GetDebuffDisplayInfoTable() do - if not colors[debuffType] then - colors[debuffType] = CreateColor(info.color.r, info.color.g, info.color.b) - end -end - -Filger.colors = colors +local _, ns = ... +local Filger = ns.Filger + +-- Blizzard +local ColorMixin = _G.ColorMixin + +local color_proto = { + SetRGBA = function(self, r, g, b, a) + if (r > 1 or g > 1 or b > 1) then + r, g, b = r / 255, g / 255, b / 255 + end + + self.r = r + self[1] = self.r + self.g = g + self[2] = self.g + self.b = b + self[3] = self.b + self.a = a or 1 + + -- pre-generate the hex color, there's no point to this being generated on the fly + self.hex = ("ff%.2x%.2x%.2x"):format(self:GetRGBAsBytes()); + end, + SetAtlas = function(self, atlas) + self.atlas = atlas + end, + GetAtlas = function(self) + return self.atlas + end, + GenerateHexColor = function(self) + return self.hex + end +} + +function Filger:CreateColor(r, g, b, a) + local color = Mixin({}, ColorMixin, color_proto) + color:SetRGBA(r, g, b, a) + return color +end + +local colors = { + ["Stealable"] = Filger:CreateColor(0.93, 0.91, 0.55), + ["Bleed"] = Filger:CreateColor(0.90, 0.10, 0.10), + ["Enrage"] = Filger:CreateColor(0.78, 0.25, 0.25) +} + +for debuffType, info in next, AuraUtil.GetDebuffDisplayInfoTable() do + if not colors[debuffType] then + colors[debuffType] = CreateColor(info.color.r, info.color.g, info.color.b) + end +end + +Filger.colors = colors diff --git a/core/commands.lua b/core/commands.lua index 077c686..edbb57c 100644 --- a/core/commands.lua +++ b/core/commands.lua @@ -1,64 +1,64 @@ -local _, ns = ... -local Filger = ns.Filger - --- Blizzard -local C_Secrets = _G.C_Secrets - ------------------------------------------------------------- --- Slash Commands ------------------------------------------------------------- -local commands = {} - -SLASH_FILGER1 = "/filger" -SlashCmdList["FILGER"] = function(cmd) - local msg = cmd:gsub("^ +", "") - local command, arg = string.split(" ", msg, 2) - arg = arg and arg:gsub(" ", "") - - -- empty or unknown command: show help - local entry = commands[command] or commands["help"] - entry.func(arg) -end - -function Filger:AddCommand(command, handler, description) - commands[command] = { - func = handler, - description = description - } -end - -local help = function() - for cmd, v in next, commands do - if (v.description) then - print(Filger.title, "|cffff8000" .. cmd .. "|r", "-", v.description) - end - end -end - -Filger:AddCommand("help", help, "list commands") - --- lists the unit's own auras with their spell IDs, to fill data//spells.lua --- usage: /filger auras player buffs (arg arrives without spaces: "playerbuffs") -local auras = function(arg) - local unit, kind = (arg or ""):match("^(%a-)(d?e?buffs)$") - if not unit or unit == "" or (kind ~= "buffs" and kind ~= "debuffs") then - print(Filger.title, "usage: /filger auras ") - return - end - - -- tainted code can't read secret auras (in combat on Retail), GetAuraSlots would error - if C_Secrets and C_Secrets.ShouldAurasBeSecret() then - print(Filger.title, "auras are secret right now, leave combat and try again.") - return - end - - local filter = ((kind == "buffs") and "HELPFUL" or "HARMFUL") .. "|PLAYER" - for _, slot in ipairs({ select(2, C_UnitAuras.GetAuraSlots(unit, filter)) }) do - local data = C_UnitAuras.GetAuraDataBySlot(unit, slot) - if data then - print(Filger.title, data.spellId, data.name) - end - end -end - -Filger:AddCommand("auras", auras, "list your auras on a unit with their spell IDs (/filger auras player buffs)") +local _, ns = ... +local Filger = ns.Filger + +-- Blizzard +local C_Secrets = _G.C_Secrets + +------------------------------------------------------------ +-- Slash Commands +------------------------------------------------------------ +local commands = {} + +SLASH_FILGER1 = "/filger" +SlashCmdList["FILGER"] = function(cmd) + local msg = cmd:gsub("^ +", "") + local command, arg = string.split(" ", msg, 2) + arg = arg and arg:gsub(" ", "") + + -- empty or unknown command: show help + local entry = commands[command] or commands["help"] + entry.func(arg) +end + +function Filger:AddCommand(command, handler, description) + commands[command] = { + func = handler, + description = description + } +end + +local help = function() + for cmd, v in next, commands do + if (v.description) then + print(Filger.title, "|cffff8000" .. cmd .. "|r", "-", v.description) + end + end +end + +Filger:AddCommand("help", help, "list commands") + +-- lists the unit's own auras with their spell IDs, to fill data//spells.lua +-- usage: /filger auras player buffs (arg arrives without spaces: "playerbuffs") +local auras = function(arg) + local unit, kind = (arg or ""):match("^(%a-)(d?e?buffs)$") + if not unit or unit == "" or (kind ~= "buffs" and kind ~= "debuffs") then + print(Filger.title, "usage: /filger auras ") + return + end + + -- tainted code can't read secret auras (in combat on Retail), GetAuraSlots would error + if C_Secrets and C_Secrets.ShouldAurasBeSecret() then + print(Filger.title, "auras are secret right now, leave combat and try again.") + return + end + + local filter = ((kind == "buffs") and "HELPFUL" or "HARMFUL") .. "|PLAYER" + for _, slot in ipairs({ select(2, C_UnitAuras.GetAuraSlots(unit, filter)) }) do + local data = C_UnitAuras.GetAuraDataBySlot(unit, slot) + if data then + print(Filger.title, data.spellId, data.name) + end + end +end + +Filger:AddCommand("auras", auras, "list your auras on a unit with their spell IDs (/filger auras player buffs)") diff --git a/core/element.lua b/core/element.lua new file mode 100644 index 0000000..d721add --- /dev/null +++ b/core/element.lua @@ -0,0 +1,138 @@ +local _, ns = ... +local Filger = ns.Filger + +-- Blizzard +local CreateFrame = _G.CreateFrame + +-------------------------------------------------- +-- FILGER +-------------------------------------------------- + +do + local element_proto = {} + + function element_proto:SetPosition(from, to) + local element = self + local sizex = element.size + element.spacing + local sizey = element.size + element.spacing + local anchor = element.initialAnchor or "BOTTOMLEFT" + local growthx = (element["growth-x"] == "LEFT" and -1) or 1 + local growthy = (element["growth-y"] == "DOWN" and -1) or 1 + local cols = math.floor(element:GetWidth() / sizex + 0.5) + + for i = from, to do + local button = element[i] + if (not button) then break end + + if anchor == "CENTER" then + -- Centralized positioning logic + local index = i - from + local col, row + if index % 2 == 0 then + col = math.floor(index / 2) + else + col = -math.floor(index / 2) - 1 + end + row = math.floor(index / cols) + + button:ClearAllPoints() + button:SetPoint(anchor, element, anchor, col * sizex, row * sizey) + else + local col = (i - 1) % cols + local row = math.floor((i - 1) / cols) + + button:ClearAllPoints() + button:SetPoint(anchor, element, anchor, col * sizex * growthx, row * sizey * growthy) + end + end + end + + function element_proto:AcquireButton(index) + local button = self[index] + if not button then + button = Filger:CreateButton(self, index) + table.insert(self, button) + end + return button + end + + function element_proto:Render(unit, sorted, compare) + table.sort(sorted, compare) + + local numVisible = math.min(self.limit, #sorted) + + for i = 1, numVisible do + self:Update(unit, sorted[i], i) + end + + for i = numVisible + 1, #self do + self[i]:Hide() + end + + self:SetPosition(1, numVisible) + end + + function Filger:Spawn(key, data) + -- "PLAYER_AURAS" -> "FilgerPlayerAuras" + local name = key:lower():gsub("^%l", string.upper):gsub("_(%l)", string.upper) + local element = Mixin(CreateFrame("Frame", self:GetName() .. name, self), element_proto) + + -- configuration + element.unit = data.unit or "player" + element.filter = data.filter or "HELPFUL" + + element.limit = data.limit or 32 + element.size = data.size or 32 + element.spacing = data.spacing or 5 + element.initialAnchor = data.initialAnchor or "BOTTOMLEFT" + element["growth-x"] = data["growth-x"] or "LEFT" + element["growth-y"] = data["growth-y"] or "DOWN" + + element:SetPoint(unpack(data.anchor)) + element:SetWidth((element.limit * element.size) + (element.limit - 1) * element.spacing) + element:SetHeight(element.size) + + if (data.type == "cooldowns") then + element = Mixin(element, ns.cooldown_proto) + + -- overrider element functions with configurations + element.SortCooldowns = data.SortCooldowns or element.SortCooldowns + element.FilterCooldown = data.FilterCooldown or element.FilterCooldown + + element.casted = {} + element.active = {} + + element:RegisterEvent("SPELL_UPDATE_COOLDOWN") + element:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", self.unit) + elseif (data.type == "container") then + -- Mainline only: Blizzard's secure aura container filters and displays the auras + element = Mixin(element, ns.container_proto) + element:CreateContainer(data) + elseif (data.type == "auras") then + element = Mixin(element, ns.aura_proto) + + -- overrider element functions with configurations + element.SortAuras = data.SortAuras or element.SortAuras + element.FilterAura = data.FilterAura or element.FilterAura + + element:RegisterEvent("UNIT_AURA") + + if (element.unit == "target") then + element:RegisterEvent("PLAYER_TARGET_CHANGED") + end + + if (element.unit == "focus") then + element:RegisterEvent("PLAYER_FOCUS_CHANGED") + end + end + + element.spells = data.spells or {} + + element:SetScript("OnEvent", function(self, event, ...) + -- call event handler + self[event](self, ...) + end) + + return element + end +end diff --git a/core/init.lua b/core/init.lua index 8bba237..67f351a 100644 --- a/core/init.lua +++ b/core/init.lua @@ -1,38 +1,38 @@ -local addon, ns = ... - -local frame = CreateFrame("Frame", "Filger", UIParent) - --- global -_G[addon] = frame - --- blizzard -local GetAddOnMetadata = C_AddOns and C_AddOns.GetAddOnMetadata or _G.GetAddOnMetadata - -local _, physicalScreenHeight = GetPhysicalScreenSize() - --- System -frame.pixelPerfectScale = math.min(1, math.max(0.3, 768 / physicalScreenHeight)) - --- addon -frame.title = GetAddOnMetadata(addon, "Title") -frame.version = GetAddOnMetadata(addon, "Version") - --- interface --- reference: https://warcraft.wiki.gg/wiki/WOW_PROJECT_ID -frame.isStandard = (WOW_PROJECT_ID == WOW_PROJECT_MAINLINE) -frame.isVanilla = (WOW_PROJECT_ID == WOW_PROJECT_CLASSIC) -frame.isTBC = (WOW_PROJECT_ID == WOW_PROJECT_BURNING_CRUSADE_CLASSIC) -frame.isWrath = (WOW_PROJECT_ID == WOW_PROJECT_WRATH_CLASSIC) -frame.isCata = (WOW_PROJECT_ID == WOW_PROJECT_CATACLYSM_CLASSIC) -frame.isMists = (WOW_PROJECT_ID == WOW_PROJECT_MISTS_CLASSIC) -frame.isCamelot = (WOW_PROJECT_ID == WOW_PROJECT_CAMELOT) - --- game type families, as in TOC [AllowLoadGameType mainline/classic] -frame.isClassic = (frame.isVanilla or frame.isTBC or frame.isWrath or frame.isCata or frame.isMists) -frame.isMainline = not frame.isClassic - --- player -frame.name = UnitName("player") -frame.class = select(2, UnitClass("player")) - -ns.Filger = frame +local addon, ns = ... + +local frame = CreateFrame("Frame", "Filger", UIParent) + +-- global +_G[addon] = frame + +-- blizzard +local GetAddOnMetadata = C_AddOns and C_AddOns.GetAddOnMetadata or _G.GetAddOnMetadata + +local _, physicalScreenHeight = GetPhysicalScreenSize() + +-- System +frame.pixelPerfectScale = math.min(1, math.max(0.3, 768 / physicalScreenHeight)) + +-- addon +frame.title = GetAddOnMetadata(addon, "Title") +frame.version = GetAddOnMetadata(addon, "Version") + +-- interface +-- reference: https://warcraft.wiki.gg/wiki/WOW_PROJECT_ID +frame.isStandard = (WOW_PROJECT_ID == WOW_PROJECT_MAINLINE) +frame.isVanilla = (WOW_PROJECT_ID == WOW_PROJECT_CLASSIC) +frame.isTBC = (WOW_PROJECT_ID == WOW_PROJECT_BURNING_CRUSADE_CLASSIC) +frame.isWrath = (WOW_PROJECT_ID == WOW_PROJECT_WRATH_CLASSIC) +frame.isCata = (WOW_PROJECT_ID == WOW_PROJECT_CATACLYSM_CLASSIC) +frame.isMists = (WOW_PROJECT_ID == WOW_PROJECT_MISTS_CLASSIC) +frame.isCamelot = (WOW_PROJECT_ID == WOW_PROJECT_CAMELOT) + +-- game type families, as in TOC [AllowLoadGameType mainline/classic] +frame.isClassic = (frame.isVanilla or frame.isTBC or frame.isWrath or frame.isCata or frame.isMists) +frame.isMainline = not frame.isClassic + +-- player +frame.name = UnitName("player") +frame.class = select(2, UnitClass("player")) + +ns.Filger = frame diff --git a/core/init.xml b/core/init.xml index 7ee219f..97cbb23 100644 --- a/core/init.xml +++ b/core/init.xml @@ -1,9 +1,11 @@ - -