Compare commits

...

24 Commits

Author SHA1 Message Date
Torsten Brendgen
391e581d32 feat: Add Personal Auras module for tracking debuffs on the player
- Introduced a new module for Personal Auras that allows players to track selected debuffs on themselves in a movable frame.
- Implemented functionality to manage tracked debuffs, including adding and removing spells.
- Added options for configuring the appearance and behavior of the Personal Auras frame.
- Updated the readme to include information about the new Personal Auras feature.
2026-04-12 00:04:34 +02:00
Torsten Brendgen
01eeae9603 Refactor Buff Ending Announcer to Aura Expiry
- Deleted LibGroupInSpecT-1.1.toc, LibStub.lua, and lib.xml files as they are no longer needed.
- Updated localization files (deDE.lua, enUS.lua) to reflect the change from "Buff Ending" to "Aura Expiry".
- Renamed BuffEndingAnnouncer module to AuraExpiry and updated references throughout the codebase.
- Adjusted options and configuration to align with the new module name.
- Updated readme to describe the new Aura Expiry feature instead of Buff Ending Announcer.
2026-04-11 23:21:34 +02:00
Torsten Brendgen
8f2f66e3a6 Update versioning in HailMaryGuildTools.toc to 2.0.0 and 2.1.0-beta 2026-04-11 22:52:27 +02:00
Torsten Brendgen
340b9c63d0 init 2.0.0 2026-04-11 22:51:14 +02:00
Torsten Brendgen
e0997eeb51 initial creation 2026-04-11 22:50:59 +02:00
a451b1f2a0 Merge pull request 'torsten-patch-1' (#2) from torsten-patch-1 into main
All checks were successful
Tag from TOC Version / tag (push) Successful in 6s
Reviewed-on: http://10.10.2.140:3000/Torsten/HailMaryGuildTools/pulls/2
2026-04-11 20:41:50 +00:00
d00e29f99e .gitea/workflows/release-from-tag hinzugefügt 2026-04-11 20:39:03 +00:00
73f662c546 .gitea/workflows/release.yml gelöscht 2026-04-11 20:38:43 +00:00
5721ed7496 .gitea/workflows/tag-on-main.yml gelöscht 2026-04-11 20:38:25 +00:00
4e8536ec51 .gitea/workflows/tag-from-toc.yml hinzugefügt 2026-04-11 20:37:59 +00:00
582eedb641 Merge pull request 'v.2.0' (#1) from v.2.0 into main
All checks were successful
Auto Tag on Main / tag (push) Successful in 7s
Build Release ZIP / release (push) Successful in 9s
Reviewed-on: http://10.10.2.140:3000/Torsten/HailMaryGuildTools/pulls/1
2026-04-11 20:26:41 +00:00
Torsten Brendgen
64e1ef0e6a Adding Pipeline for release and tag on main 2026-04-11 22:25:16 +02:00
Torsten Brendgen
45ef178c37 update workflow
All checks were successful
Build and Release / zip (push) Successful in 5s
2026-04-10 23:46:46 +02:00
Torsten Brendgen
a68e5861e9 update workflow
Some checks failed
Build and Release / zip (push) Failing after 5s
2026-04-10 23:42:05 +02:00
Torsten Brendgen
da068c3290 update workflow
Some checks failed
Build + Release / zip (push) Failing after 5s
2026-04-10 23:39:51 +02:00
Torsten Brendgen
05f9ddc2df Build and Release v1.2.0
Some checks failed
Build + Release / zip (push) Failing after 7s
2026-04-10 23:35:25 +02:00
Torsten Brendgen
3bdf167b6f Build and Release v1.1.0
All checks were successful
Build + Release / release (push) Successful in 1s
2026-04-10 23:29:49 +02:00
Torsten Brendgen
fcc671b920 Build and Release v1.0.0 2026-04-10 23:27:51 +02:00
Torsten Brendgen
1d08895374 trigger workflow 2026-04-10 23:27:10 +02:00
Torsten Brendgen
2c605c98cd Adding Build + Release Workflow 2026-04-10 23:24:55 +02:00
Torsten Brendgen
47adcaa82c trigger workflow
All checks were successful
Pack ZIP / zip (push) Successful in 1s
2026-04-10 23:23:00 +02:00
Torsten Brendgen
ebe84692c5 Adding Zip-Pack Workflow
All checks were successful
Pack ZIP / zip (push) Successful in 2s
2026-04-10 23:14:39 +02:00
Torsten Brendgen
2c902ae3c0 trigger workflow
Some checks failed
Pack ZIP / zip (push) Failing after 0s
2026-04-10 23:11:11 +02:00
Torsten Brendgen
5462c7b22f trigger workflow 2026-04-10 23:10:50 +02:00
21 changed files with 1201 additions and 1441 deletions

View File

@@ -1,18 +0,0 @@
name: ARM Test
on:
push:
jobs:
test:
runs-on: debian-12
steps:
- name: Architektur prüfen
run: |
uname -m
arch
- name: OS prüfen
run: |
cat /etc/os-release

View File

@@ -0,0 +1,74 @@
name: Build Release ZIP
on:
push:
tags:
- "v*"
jobs:
release:
runs-on: debian-12
steps:
- name: Tools installieren
run: |
apt-get update
apt-get install -y git zip curl jq rsync
- name: Repo klonen
run: |
rm -rf /tmp/repo /tmp/build
git clone http://oauth2:${{ secrets.PAT_TOKEN }}@10.10.2.140:3000/Torsten/HailMaryGuildTools.git /tmp/repo
- name: ZIP mit Addon-Ordner bauen
run: |
set -e
mkdir -p /tmp/build/HailMaryGuildTools
rsync -a \
--exclude='.git' \
--exclude='.gitea' \
/tmp/repo/ /tmp/build/HailMaryGuildTools/
cd /tmp/build
zip -r "/tmp/HailMaryGuildTools-${{ gitea.ref_name }}.zip" HailMaryGuildTools
ls -lh "/tmp/HailMaryGuildTools-${{ gitea.ref_name }}.zip"
- name: Release anlegen oder laden
run: |
set -e
TAG="${{ gitea.ref_name }}"
API="http://10.10.2.140:3000/api/v1/repos/Torsten/HailMaryGuildTools"
HTTP_CODE=$(curl -s -o /tmp/release.json -w "%{http_code}" \
-H "Authorization: token ${{ secrets.PAT_TOKEN }}" \
"$API/releases/tags/$TAG")
if [ "$HTTP_CODE" = "200" ]; then
echo "Release existiert bereits."
else
echo "Release wird erstellt."
curl --fail -s \
-H "Authorization: token ${{ secrets.PAT_TOKEN }}" \
-H "Content-Type: application/json" \
-d "{
\"tag_name\": \"$TAG\",
\"name\": \"HailMaryGuildTools $TAG\",
\"draft\": false,
\"prerelease\": false
}" \
"$API/releases" > /tmp/release.json
fi
jq '.id, .tag_name, .html_url' /tmp/release.json
- name: ZIP an Release anhängen
run: |
set -e
RELEASE_ID=$(jq -r '.id' /tmp/release.json)
FILE="/tmp/HailMaryGuildTools-${{ gitea.ref_name }}.zip"
curl --fail \
-H "Authorization: token ${{ secrets.PAT_TOKEN }}" \
-F "attachment=@${FILE}" \
"http://10.10.2.140:3000/api/v1/repos/Torsten/HailMaryGuildTools/releases/${RELEASE_ID}/assets?name=$(basename "$FILE")"

View File

@@ -0,0 +1,66 @@
name: Tag from TOC Version
on:
push:
branches:
- main
jobs:
tag:
runs-on: debian-12
steps:
- name: Tools installieren
run: |
apt-get update
apt-get install -y git grep sed
- name: Repo klonen
run: |
rm -rf /tmp/repo
git clone http://oauth2:${{ secrets.PAT_TOKEN }}@10.10.2.140:3000/Torsten/HailMaryGuildTools.git /tmp/repo
- name: Version aus TOC lesen
run: |
set -e
cd /tmp/repo
TOC_FILE="HailMaryGuildTools.toc"
if [ ! -f "$TOC_FILE" ]; then
echo "TOC-Datei nicht gefunden: $TOC_FILE"
exit 1
fi
VERSION=$(grep '^## Version:' "$TOC_FILE" | sed 's/^## Version:[[:space:]]*//')
if [ -z "$VERSION" ]; then
echo "Keine Version in $TOC_FILE gefunden"
exit 1
fi
TAG="v$VERSION"
echo "Gefundene Version: $VERSION"
echo "Neues Tag wäre: $TAG"
echo "$TAG" > /tmp/release_tag.txt
- name: Tag nur erstellen, wenn noch nicht vorhanden
run: |
set -e
cd /tmp/repo
git fetch --tags
TAG=$(cat /tmp/release_tag.txt)
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "Tag $TAG existiert bereits, nichts zu tun."
exit 0
fi
git config user.name "Gitea Actions"
git config user.email "actions@local"
git tag "$TAG"
git push origin "$TAG"

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.git
.gitea
.vscode
*.zip

View File

@@ -29,7 +29,7 @@
<!-- AceGUI -->
<Include file="Libs\AceGUI-3.0\AceGUI-3.0.xml"/>
<!-- AceConfig (depends on AceGUI via AceConfigDialog) -->
<!-- AceConfig -->
<Include file="Libs\AceConfig-3.0\AceConfig-3.0.xml"/>
<!-- AceConsole -->

View File

@@ -223,6 +223,17 @@ local defaults = {
announceAtSec = 5,
trackedBuffs = {},
},
personalAuras = {
enabled = false,
unlocked = false,
posX = 0,
posY = 120,
width = 260,
rowHeight = 24,
iconSize = 20,
fontSize = 12,
trackedDebuffs = {},
},
raidTimeline = {
enabled = false,
leadTime = 5,
@@ -1254,6 +1265,30 @@ local function NormalizeBuffEndingAnnouncerSettings(settings)
settings.trackedBuffs = normalized
end
local function NormalizePersonalAurasSettings(settings)
if type(settings) ~= "table" then return end
if settings.enabled == nil then settings.enabled = false end
settings.unlocked = settings.unlocked == true
settings.posX = NormalizeLayoutValue(settings.posX, -2000, 2000, 0)
settings.posY = NormalizeLayoutValue(settings.posY, -2000, 2000, 120)
settings.width = math.floor(NormalizeLayoutValue(settings.width, 160, 420, 260) + 0.5)
settings.rowHeight = math.floor(NormalizeLayoutValue(settings.rowHeight, 18, 42, 24) + 0.5)
settings.iconSize = math.floor(NormalizeLayoutValue(settings.iconSize, 14, 32, 20) + 0.5)
settings.fontSize = math.floor(NormalizeLayoutValue(settings.fontSize, 8, 24, 12) + 0.5)
if type(settings.trackedDebuffs) ~= "table" then
settings.trackedDebuffs = {}
return
end
local normalized = {}
for sid, value in pairs(settings.trackedDebuffs) do
local id = tonumber(sid)
if id and id > 0 and value ~= false and value ~= nil then
normalized[id] = true
end
end
settings.trackedDebuffs = normalized
end
local function NormalizeRaidTimelineSettings(settings)
if type(settings) ~= "table" then return end
if settings.enabled == nil then settings.enabled = false end
@@ -1691,6 +1726,8 @@ function HMGT:MigrateProfileSettings()
NormalizeMapOverlaySettings(p.mapOverlay)
p.buffEndingAnnouncer = p.buffEndingAnnouncer or {}
NormalizeBuffEndingAnnouncerSettings(p.buffEndingAnnouncer)
p.personalAuras = p.personalAuras or {}
NormalizePersonalAurasSettings(p.personalAuras)
p.raidTimeline = p.raidTimeline or {}
NormalizeRaidTimelineSettings(p.raidTimeline)
p.notes = p.notes or {}

View File

@@ -4,8 +4,8 @@
## Title: Hail Mary Guild Tools
## Notes: Guild Tools for World of Warcraft
## Version: 1.3
## X-Stable-Version: 1.3
## X-Build-Version: 2.0-beta
## X-Stable-Version: 2.0.0
## X-Build-Version: 2.1.0-beta
## X-Release-Channel: beta
## SavedVariables: HailMaryGuildToolsDB
## OptionalDeps: Ace3, LibSharedMedia-3.0, LibDataBroker-1.1, LibDBIcon-1.0
@@ -41,9 +41,13 @@ Modules\Tracker\NormalTrackerFrames.lua
Modules\Tracker\GroupTrackerFrames.lua
Modules\Tracker\TrackerOptions.lua
# ────── BuffEndingAnnouncer ──────────────────────────────────────────
Modules\BuffEndingAnnouncer\BuffEndingAnnouncer.lua
Modules\BuffEndingAnnouncer\BuffEndingAnnouncerOptions.lua
# ────── AuraExpiry ───────────────────────────────────────────────────
Modules\AuraExpiry\AuraExpiry.lua
Modules\AuraExpiry\AuraExpiryOptions.lua
# ────── PersonalAuras ────────────────────────────────────────────────
Modules\PersonalAuras\PersonalAuras.lua
Modules\PersonalAuras\PersonalAurasOptions.lua
# ────── MapOverlay ───────────────────────────────────────────────────
Modules\MapOverlay\MapOverlayIconConfig.lua

View File

@@ -1974,14 +1974,23 @@ function HMGT_Config:Initialize()
end
local buffEndingGroup = BuildNamedModuleGroup(
"announcer.buffEndingAnnouncer",
L["OPT_MODULE_BUFF_ENDING"] or "Buff Ending",
"announcer.auraExpiry",
L["OPT_MODULE_AURA_EXPIRY"] or L["OPT_MODULE_BUFF_ENDING"] or "Aura Expiry",
20
)
if buffEndingGroup then
modulesGroup.args.buffEnding = buffEndingGroup
end
local personalAurasGroup = BuildNamedModuleGroup(
"personalAuras",
L["OPT_MODULE_PERSONAL_AURAS"] or "Personal Auras",
25
)
if personalAurasGroup then
modulesGroup.args.personalAuras = personalAurasGroup
end
local mapOverlayGroup = BuildNamedModuleGroup(
"map.overlay",
L["OPT_MODULE_MAP_OVERLAY"] or "Map Overlay",

View File

@@ -1,23 +0,0 @@
------------------------------------------------------------------------
r106 | nebula169 | 2024-08-23 12:53:29 +0000 (Fri, 23 Aug 2024) | 1 line
Changed paths:
M /trunk/LibGroupInSpecT-1.1.lua
Update talent string decoding for v2
------------------------------------------------------------------------
r105 | nebula169 | 2024-08-20 06:44:07 +0000 (Tue, 20 Aug 2024) | 2 lines
Changed paths:
M /trunk/LibGroupInSpecT-1.1.lua
- Handle heroic talents
- Remove tier/column
------------------------------------------------------------------------
r104 | nebula169 | 2024-07-25 01:08:28 +0000 (Thu, 25 Jul 2024) | 1 line
Changed paths:
M /trunk/LibGroupInSpecT-1.1.lua
M /trunk/LibGroupInSpecT-1.1.toc
D /trunk/embeds.xml
Update TOC for 11.0
------------------------------------------------------------------------

View File

@@ -1,240 +0,0 @@
--[[ $Id: CallbackHandler-1.0.lua 14 2010-08-09 00:43:38Z mikk $ ]]
local MAJOR, MINOR = "CallbackHandler-1.0", 6
local CallbackHandler = LibStub:NewLibrary(MAJOR, MINOR)
if not CallbackHandler then return end -- No upgrade needed
local meta = {__index = function(tbl, key) tbl[key] = {} return tbl[key] end}
-- Lua APIs
local tconcat = table.concat
local assert, error, loadstring = assert, error, loadstring
local setmetatable, rawset, rawget = setmetatable, rawset, rawget
local next, select, pairs, type, tostring = next, select, pairs, type, tostring
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
-- List them here for Mikk's FindGlobals script
-- GLOBALS: geterrorhandler
local xpcall = xpcall
local function errorhandler(err)
return geterrorhandler()(err)
end
local function CreateDispatcher(argCount)
local code = [[
local next, xpcall, eh = ...
local method, ARGS
local function call() method(ARGS) end
local function dispatch(handlers, ...)
local index
index, method = next(handlers)
if not method then return end
local OLD_ARGS = ARGS
ARGS = ...
repeat
xpcall(call, eh)
index, method = next(handlers, index)
until not method
ARGS = OLD_ARGS
end
return dispatch
]]
local ARGS, OLD_ARGS = {}, {}
for i = 1, argCount do ARGS[i], OLD_ARGS[i] = "arg"..i, "old_arg"..i end
code = code:gsub("OLD_ARGS", tconcat(OLD_ARGS, ", ")):gsub("ARGS", tconcat(ARGS, ", "))
return assert(loadstring(code, "safecall Dispatcher["..argCount.."]"))(next, xpcall, errorhandler)
end
local Dispatchers = setmetatable({}, {__index=function(self, argCount)
local dispatcher = CreateDispatcher(argCount)
rawset(self, argCount, dispatcher)
return dispatcher
end})
--------------------------------------------------------------------------
-- CallbackHandler:New
--
-- target - target object to embed public APIs in
-- RegisterName - name of the callback registration API, default "RegisterCallback"
-- UnregisterName - name of the callback unregistration API, default "UnregisterCallback"
-- UnregisterAllName - name of the API to unregister all callbacks, default "UnregisterAllCallbacks". false == don't publish this API.
function CallbackHandler:New(target, RegisterName, UnregisterName, UnregisterAllName, OnUsed, OnUnused)
-- TODO: Remove this after beta has gone out
assert(not OnUsed and not OnUnused, "ACE-80: OnUsed/OnUnused are deprecated. Callbacks are now done to registry.OnUsed and registry.OnUnused")
RegisterName = RegisterName or "RegisterCallback"
UnregisterName = UnregisterName or "UnregisterCallback"
if UnregisterAllName==nil then -- false is used to indicate "don't want this method"
UnregisterAllName = "UnregisterAllCallbacks"
end
-- we declare all objects and exported APIs inside this closure to quickly gain access
-- to e.g. function names, the "target" parameter, etc
-- Create the registry object
local events = setmetatable({}, meta)
local registry = { recurse=0, events=events }
-- registry:Fire() - fires the given event/message into the registry
function registry:Fire(eventname, ...)
if not rawget(events, eventname) or not next(events[eventname]) then return end
local oldrecurse = registry.recurse
registry.recurse = oldrecurse + 1
Dispatchers[select('#', ...) + 1](events[eventname], eventname, ...)
registry.recurse = oldrecurse
if registry.insertQueue and oldrecurse==0 then
-- Something in one of our callbacks wanted to register more callbacks; they got queued
for eventname,callbacks in pairs(registry.insertQueue) do
local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten.
for self,func in pairs(callbacks) do
events[eventname][self] = func
-- fire OnUsed callback?
if first and registry.OnUsed then
registry.OnUsed(registry, target, eventname)
first = nil
end
end
end
registry.insertQueue = nil
end
end
-- Registration of a callback, handles:
-- self["method"], leads to self["method"](self, ...)
-- self with function ref, leads to functionref(...)
-- "addonId" (instead of self) with function ref, leads to functionref(...)
-- all with an optional arg, which, if present, gets passed as first argument (after self if present)
target[RegisterName] = function(self, eventname, method, ... --[[actually just a single arg]])
if type(eventname) ~= "string" then
error("Usage: "..RegisterName.."(eventname, method[, arg]): 'eventname' - string expected.", 2)
end
method = method or eventname
local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten.
if type(method) ~= "string" and type(method) ~= "function" then
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - string or function expected.", 2)
end
local regfunc
if type(method) == "string" then
-- self["method"] calling style
if type(self) ~= "table" then
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): self was not a table?", 2)
elseif self==target then
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): do not use Library:"..RegisterName.."(), use your own 'self'", 2)
elseif type(self[method]) ~= "function" then
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - method '"..tostring(method).."' not found on self.", 2)
end
if select("#",...)>=1 then -- this is not the same as testing for arg==nil!
local arg=select(1,...)
regfunc = function(...) self[method](self,arg,...) end
else
regfunc = function(...) self[method](self,...) end
end
else
-- function ref with self=object or self="addonId" or self=thread
if type(self)~="table" and type(self)~="string" and type(self)~="thread" then
error("Usage: "..RegisterName.."(self or \"addonId\", eventname, method): 'self or addonId': table or string or thread expected.", 2)
end
if select("#",...)>=1 then -- this is not the same as testing for arg==nil!
local arg=select(1,...)
regfunc = function(...) method(arg,...) end
else
regfunc = method
end
end
if events[eventname][self] or registry.recurse<1 then
-- if registry.recurse<1 then
-- we're overwriting an existing entry, or not currently recursing. just set it.
events[eventname][self] = regfunc
-- fire OnUsed callback?
if registry.OnUsed and first then
registry.OnUsed(registry, target, eventname)
end
else
-- we're currently processing a callback in this registry, so delay the registration of this new entry!
-- yes, we're a bit wasteful on garbage, but this is a fringe case, so we're picking low implementation overhead over garbage efficiency
registry.insertQueue = registry.insertQueue or setmetatable({},meta)
registry.insertQueue[eventname][self] = regfunc
end
end
-- Unregister a callback
target[UnregisterName] = function(self, eventname)
if not self or self==target then
error("Usage: "..UnregisterName.."(eventname): bad 'self'", 2)
end
if type(eventname) ~= "string" then
error("Usage: "..UnregisterName.."(eventname): 'eventname' - string expected.", 2)
end
if rawget(events, eventname) and events[eventname][self] then
events[eventname][self] = nil
-- Fire OnUnused callback?
if registry.OnUnused and not next(events[eventname]) then
registry.OnUnused(registry, target, eventname)
end
end
if registry.insertQueue and rawget(registry.insertQueue, eventname) and registry.insertQueue[eventname][self] then
registry.insertQueue[eventname][self] = nil
end
end
-- OPTIONAL: Unregister all callbacks for given selfs/addonIds
if UnregisterAllName then
target[UnregisterAllName] = function(...)
if select("#",...)<1 then
error("Usage: "..UnregisterAllName.."([whatFor]): missing 'self' or \"addonId\" to unregister events for.", 2)
end
if select("#",...)==1 and ...==target then
error("Usage: "..UnregisterAllName.."([whatFor]): supply a meaningful 'self' or \"addonId\"", 2)
end
for i=1,select("#",...) do
local self = select(i,...)
if registry.insertQueue then
for eventname, callbacks in pairs(registry.insertQueue) do
if callbacks[self] then
callbacks[self] = nil
end
end
end
for eventname, callbacks in pairs(events) do
if callbacks[self] then
callbacks[self] = nil
-- Fire OnUnused callback?
if registry.OnUnused and not next(callbacks) then
registry.OnUnused(registry, target, eventname)
end
end
end
end
end
end
return registry
end
-- CallbackHandler purposefully does NOT do explicit embedding. Nor does it
-- try to upgrade old implicit embeds since the system is selfcontained and
-- relies on closures to work.

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +0,0 @@
## Interface: 110000, 110002
## Title: Lib: GroupInSpecT-1.1
## Notes: Keeps track of group members and keeps an up-to-date cache of their specialization and talents.
## Version: 1.5.1
## Author: Anyia of HordeYakka (Jubei'Thos)
## X-Category: Library
LibStub\LibStub.lua
CallbackHandler-1.0\CallbackHandler-1.0.lua
lib.xml

View File

@@ -1,51 +0,0 @@
-- $Id: LibStub.lua 76 2007-09-03 01:50:17Z mikk $
-- LibStub is a simple versioning stub meant for use in Libraries. http://www.wowace.com/wiki/LibStub for more info
-- LibStub is hereby placed in the Public Domain
-- Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke
local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 2 -- NEVER MAKE THIS AN SVN REVISION! IT NEEDS TO BE USABLE IN ALL REPOS!
local LibStub = _G[LIBSTUB_MAJOR]
-- Check to see is this version of the stub is obsolete
if not LibStub or LibStub.minor < LIBSTUB_MINOR then
LibStub = LibStub or {libs = {}, minors = {} }
_G[LIBSTUB_MAJOR] = LibStub
LibStub.minor = LIBSTUB_MINOR
-- LibStub:NewLibrary(major, minor)
-- major (string) - the major version of the library
-- minor (string or number ) - the minor version of the library
--
-- returns nil if a newer or same version of the lib is already present
-- returns empty library object or old library object if upgrade is needed
function LibStub:NewLibrary(major, minor)
assert(type(major) == "string", "Bad argument #2 to `NewLibrary' (string expected)")
minor = assert(tonumber(strmatch(minor, "%d+")), "Minor version must either be a number or contain a number.")
local oldminor = self.minors[major]
if oldminor and oldminor >= minor then return nil end
self.minors[major], self.libs[major] = minor, self.libs[major] or {}
return self.libs[major], oldminor
end
-- LibStub:GetLibrary(major, [silent])
-- major (string) - the major version of the library
-- silent (boolean) - if true, library is optional, silently return nil if its not found
--
-- throws an error if the library can not be found (except silent is set)
-- returns the library object if found
function LibStub:GetLibrary(major, silent)
if not self.libs[major] and not silent then
error(("Cannot find a library instance of %q."):format(tostring(major)), 2)
end
return self.libs[major], self.minors[major]
end
-- LibStub:IterateLibraries()
--
-- Returns an iterator for the currently registered libraries
function LibStub:IterateLibraries()
return pairs(self.libs)
end
setmetatable(LibStub, { __call = LibStub.GetLibrary })
end

View File

@@ -1,6 +0,0 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="LibGroupInSpecT-1.1.lua"/>
</Ui>

View File

@@ -116,6 +116,7 @@ L["OPT_MAP_POI_REMOVE_INDEX"] = "Index entfernen"
L["OPT_MAP_POI_REMOVE"] = "POI entfernen"
L["OPT_MAP_POI_LIST"] = "Aktuelle POIs"
L["OPT_MAP_POI_EMPTY"] = "Keine POIs konfiguriert."
L["OPT_MAP_POI_SELECT_HINT"] = "Waehle links im Baum einen POI aus, um ihn zu bearbeiten."
L["OPT_MAP_POI_CURRENT_SET"] = "HMGT: aktuelle Position uebernommen"
L["OPT_MAP_POI_CURRENT_FAILED"] = "HMGT: aktuelle Position konnte nicht ermittelt werden"
L["OPT_MAP_POI_ADDED"] = "HMGT: POI hinzugefuegt"
@@ -129,7 +130,9 @@ L["OPT_SHOW_MINIMAP_ICON"] = "Minimap-Icon anzeigen"
L["OPT_COMMANDS"] = "Commands"
L["OPT_MODULES"] = "Modules"
L["OPT_MODULE_TRACKER"] = "Tracker"
L["OPT_MODULE_BUFF_ENDING"] = "Buff Ending"
L["OPT_MODULE_BUFF_ENDING"] = "Aura-Ablauf"
L["OPT_MODULE_AURA_EXPIRY"] = "Aura-Ablauf"
L["OPT_MODULE_PERSONAL_AURAS"] = "Persoenliche Auren"
L["OPT_MODULE_MAP_OVERLAY"] = "Map Overlay"
-- ── Options: tracker shared ───────────────────────────────────
@@ -341,11 +344,13 @@ L["GCD_TITLE"] = "|cff77dd77Gruppen-Cooldowns|r"
L["IT_NAME"] = "Interrupt Tracker"
L["RCD_NAME"] = "Raid Cooldown Tracker"
L["GCD_NAME"] = "Gruppen-Cooldown-Tracker"
L["BEA_NAME"] = "Buff-Ende-Ansager"
L["BEA_NAME"] = "Aura-Ablauf"
L["AE_NAME"] = "Aura-Ablauf"
L["PA_NAME"] = "Persoenliche Auren"
L["AEM_NAME"] = "Auto-Gegner-Markierung"
L["OPT_BEA_ENABLED"] = "Buff-Ende-Ansager aktivieren"
L["OPT_BEA_ENABLED_DESC"] = "Sagt Countdown fuer verfolgte Buffs in /say an"
L["OPT_BEA_ENABLED"] = "Aura-Ablauf aktivieren"
L["OPT_BEA_ENABLED_DESC"] = "Sagt Countdowns fuer verfolgte Auren und Kanaele in /say an"
L["OPT_BEA_ANNOUNCE_AT"] = "Ansage ab (Sek.)"
L["OPT_BEA_ANNOUNCE_AT_DESC"] = "Startet die Countdown-Ansage, sobald die Restdauer darunter liegt"
L["OPT_BEA_DEFAULT_THRESHOLD"] = "Standard-Threshold (Sek.)"
@@ -369,6 +374,27 @@ L["OPT_BEA_MSG_REMOVED"] = "HMGT: Buff entfernt: %s"
L["OPT_BEA_MSG_INVALID"] = "HMGT: ungueltige Buff-Spell-ID"
L["OPT_BEA_MSG_NOT_FOUND"] = "HMGT: Buff nicht gefunden"
L["OPT_BEA_MSG_THRESHOLD_INVALID"] = "HMGT: ungueltiger Threshold"
L["OPT_PA_ENABLED"] = "Persoenliche Auren aktivieren"
L["OPT_PA_ENABLED_DESC"] = "Zeigt verfolgte Debuffs auf deinem aktuellen Spieler in einem verschiebbaren Frame an"
L["OPT_PA_UNLOCK"] = "Frame entsperren"
L["OPT_PA_UNLOCK_DESC"] = "Zeigt den Platzhalter an und erlaubt das Verschieben des Frames"
L["OPT_PA_WIDTH"] = "Frame-Breite"
L["OPT_PA_ROW_HEIGHT"] = "Zeilenhoehe"
L["OPT_PA_ICON_SIZE"] = "Icon-Groesse"
L["OPT_PA_FONT_SIZE"] = "Schriftgroesse"
L["OPT_PA_SECTION_GENERAL"] = "Allgemein"
L["OPT_PA_SECTION_DEBUFFS"] = "Verfolgte Debuffs"
L["OPT_PA_ADD_ID"] = "Spell-ID hinzufuegen"
L["OPT_PA_ADD"] = "Debuff hinzufuegen"
L["OPT_PA_REMOVE"] = "Debuff entfernen"
L["OPT_PA_EMPTY"] = "Keine Debuffs konfiguriert."
L["OPT_PA_UNLOCK_HINT"] = "Persoenliche Auren\nZum Verschieben ziehen"
L["OPT_PA_MSG_ADDED"] = "HMGT: Debuff hinzugefuegt: %s"
L["OPT_PA_MSG_REMOVED"] = "HMGT: Debuff entfernt: %s"
L["OPT_PA_MSG_INVALID"] = "HMGT: ungueltige Debuff-Spell-ID"
L["OPT_PA_MSG_NOT_FOUND"] = "HMGT: Debuff nicht gefunden"
L["OPT_PA_ACTIVE"] = "Aktiv"
L["OPT_PA_INACTIVE"] = "Inaktiv"
L["BEA_MSG_TEMPLATE"] = "%s endet in %d"
L["OPT_AEM_ENABLED"] = "Automatische Gegner-Markierung aktivieren"

View File

@@ -116,6 +116,7 @@ L["OPT_MAP_POI_REMOVE_INDEX"] = "Remove index"
L["OPT_MAP_POI_REMOVE"] = "Remove POI"
L["OPT_MAP_POI_LIST"] = "Current POIs"
L["OPT_MAP_POI_EMPTY"] = "No POIs configured."
L["OPT_MAP_POI_SELECT_HINT"] = "Select a POI in the tree on the left to edit it."
L["OPT_MAP_POI_CURRENT_SET"] = "HMGT: current position copied"
L["OPT_MAP_POI_CURRENT_FAILED"] = "HMGT: could not determine current position"
L["OPT_MAP_POI_ADDED"] = "HMGT: POI added"
@@ -129,7 +130,9 @@ L["OPT_SHOW_MINIMAP_ICON"] = "Show Minimap Icon"
L["OPT_COMMANDS"] = "Commands"
L["OPT_MODULES"] = "Modules"
L["OPT_MODULE_TRACKER"] = "Tracker"
L["OPT_MODULE_BUFF_ENDING"] = "Buff Ending"
L["OPT_MODULE_BUFF_ENDING"] = "Aura Expiry"
L["OPT_MODULE_AURA_EXPIRY"] = "Aura Expiry"
L["OPT_MODULE_PERSONAL_AURAS"] = "Personal Auras"
L["OPT_MODULE_MAP_OVERLAY"] = "Map Overlay"
-- ── Options: tracker shared ───────────────────────────────────
@@ -341,11 +344,13 @@ L["GCD_TITLE"] = "|cff77dd77Group Cooldowns|r"
L["IT_NAME"] = "Interrupt Tracker"
L["RCD_NAME"] = "Raid Cooldown Tracker"
L["GCD_NAME"] = "Group Cooldown Tracker"
L["BEA_NAME"] = "Buff Ending Announcer"
L["BEA_NAME"] = "Aura Expiry"
L["AE_NAME"] = "Aura Expiry"
L["PA_NAME"] = "Personal Auras"
L["AEM_NAME"] = "Auto Enemy Marker"
L["OPT_BEA_ENABLED"] = "Enable buff ending announcer"
L["OPT_BEA_ENABLED_DESC"] = "Announce tracked buff countdowns in /say"
L["OPT_BEA_ENABLED"] = "Enable aura expiry"
L["OPT_BEA_ENABLED_DESC"] = "Announce countdowns for tracked auras and channels in /say"
L["OPT_BEA_ANNOUNCE_AT"] = "Announce from (sec)"
L["OPT_BEA_ANNOUNCE_AT_DESC"] = "Start countdown announcements when remaining buff duration is at or below this value"
L["OPT_BEA_DEFAULT_THRESHOLD"] = "Default threshold (sec)"
@@ -369,6 +374,27 @@ L["OPT_BEA_MSG_REMOVED"] = "HMGT: buff removed: %s"
L["OPT_BEA_MSG_INVALID"] = "HMGT: invalid buff spell ID"
L["OPT_BEA_MSG_NOT_FOUND"] = "HMGT: buff not found"
L["OPT_BEA_MSG_THRESHOLD_INVALID"] = "HMGT: invalid threshold"
L["OPT_PA_ENABLED"] = "Enable personal auras"
L["OPT_PA_ENABLED_DESC"] = "Show tracked debuffs on your current player in a movable frame"
L["OPT_PA_UNLOCK"] = "Unlock frame"
L["OPT_PA_UNLOCK_DESC"] = "Show the frame placeholder and allow it to be moved"
L["OPT_PA_WIDTH"] = "Frame width"
L["OPT_PA_ROW_HEIGHT"] = "Row height"
L["OPT_PA_ICON_SIZE"] = "Icon size"
L["OPT_PA_FONT_SIZE"] = "Font size"
L["OPT_PA_SECTION_GENERAL"] = "General"
L["OPT_PA_SECTION_DEBUFFS"] = "Tracked debuffs"
L["OPT_PA_ADD_ID"] = "Add Spell ID"
L["OPT_PA_ADD"] = "Add debuff"
L["OPT_PA_REMOVE"] = "Remove debuff"
L["OPT_PA_EMPTY"] = "No debuffs configured."
L["OPT_PA_UNLOCK_HINT"] = "Personal Auras\nDrag to move"
L["OPT_PA_MSG_ADDED"] = "HMGT: debuff added: %s"
L["OPT_PA_MSG_REMOVED"] = "HMGT: debuff removed: %s"
L["OPT_PA_MSG_INVALID"] = "HMGT: invalid debuff spell ID"
L["OPT_PA_MSG_NOT_FOUND"] = "HMGT: debuff not found"
L["OPT_PA_ACTIVE"] = "Active"
L["OPT_PA_INACTIVE"] = "Inactive"
L["BEA_MSG_TEMPLATE"] = "%s ending in %d"
L["OPT_AEM_ENABLED"] = "Enable auto enemy marking"

View File

@@ -1,4 +1,4 @@
-- Modules/BuffEndingAnnouncer/BuffEndingAnnouncer.lua
-- Modules/AuraExpiry/AuraExpiry.lua
-- Announces tracked buffs in SAY shortly before they expire.
local ADDON_NAME = "HailMaryGuildTools"
@@ -7,7 +7,8 @@ if not HMGT then return end
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME, true) or {}
local BEA = HMGT:NewModule("BuffEndingAnnouncer")
local BEA = HMGT:NewModule("AuraExpiry")
HMGT.AuraExpiry = BEA
HMGT.BuffEndingAnnouncer = BEA
BEA.runtimeEnabled = false
@@ -426,6 +427,7 @@ function BEA:StopRuntime()
end
function BEA:OnInitialize()
HMGT.AuraExpiry = self
HMGT.BuffEndingAnnouncer = self
end

View File

@@ -1,9 +1,9 @@
-- Modules/BuffEndingAnnouncer/BuffEndingAnnouncerOptions.lua
-- Modules/AuraExpiry/AuraExpiryOptions.lua
local ADDON_NAME = "HailMaryGuildTools"
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
if not HMGT then return end
local BEA = HMGT.BuffEndingAnnouncer
local BEA = HMGT.AuraExpiry or HMGT.BuffEndingAnnouncer
if not BEA then return end
if not HMGT_Config or not HMGT_Config.RegisterOptionsProvider then return end
@@ -197,7 +197,7 @@ function BEA:BuildOptionsGroup()
local group = {
type = "group",
name = L["BEA_NAME"] or "Buff Ending Announcer",
name = L["AE_NAME"] or L["BEA_NAME"] or "Aura Expiry",
order = 3,
childGroups = "tree",
args = {
@@ -400,7 +400,7 @@ function BEA:GetOptionsGroup()
return beaOptionsGroup
end
HMGT_Config:RegisterOptionsProvider("announcer.buffEndingAnnouncer", function()
HMGT_Config:RegisterOptionsProvider("announcer.auraExpiry", function()
return {
path = "announcer",
order = 3,

View File

@@ -0,0 +1,612 @@
local ADDON_NAME = "HailMaryGuildTools"
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
if not HMGT then return end
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME, true) or {}
local PA = HMGT:NewModule("PersonalAuras")
HMGT.PersonalAuras = PA
PA.runtimeEnabled = false
PA.eventFrame = nil
PA.ticker = nil
PA.frame = nil
PA.rows = nil
local function ClampNumber(value, minValue, maxValue, fallback)
local numericValue = tonumber(value)
if numericValue == nil then
return fallback
end
if numericValue < minValue then
return minValue
end
if numericValue > maxValue then
return maxValue
end
return numericValue
end
local function GetSpellName(spellId)
local sid = tonumber(spellId)
if not sid or sid <= 0 then return nil end
if C_Spell and type(C_Spell.GetSpellName) == "function" then
local name = C_Spell.GetSpellName(sid)
if type(name) == "string" and name ~= "" then
return name
end
end
if type(GetSpellInfo) == "function" then
local name = GetSpellInfo(sid)
if type(name) == "string" and name ~= "" then
return name
end
end
return nil
end
local function GetSpellIcon(spellId)
local sid = tonumber(spellId)
if not sid or sid <= 0 then return nil end
if HMGT_SpellData and type(HMGT_SpellData.GetSpellIcon) == "function" then
local icon = HMGT_SpellData.GetSpellIcon(sid)
if icon and icon ~= "" then
return icon
end
end
if C_Spell and type(C_Spell.GetSpellTexture) == "function" then
local icon = C_Spell.GetSpellTexture(sid)
if icon and icon ~= "" then
return icon
end
end
if type(GetSpellInfo) == "function" then
local _, _, icon = GetSpellInfo(sid)
if icon and icon ~= "" then
return icon
end
end
return 136243
end
local function NormalizeAuraApplications(auraData)
if type(auraData) ~= "table" then
return 0
end
local count = tonumber(auraData.applications or auraData.stackCount or auraData.stacks or auraData.charges or auraData.count)
if count ~= nil then
return math.max(0, count)
end
return 0
end
local function FormatRemainingTime(secondsRemaining)
local seconds = tonumber(secondsRemaining)
if not seconds or seconds <= 0 then
return ""
end
if seconds >= 60 then
local total = math.floor(seconds + 0.5)
local minutes = math.floor(total / 60)
local rest = total % 60
return string.format("%d:%02d", minutes, rest)
end
if seconds >= 10 then
return string.format("%ds", math.floor(seconds + 0.5))
end
return string.format("%.1fs", seconds)
end
local function NormalizeSettings(settings)
if type(settings) ~= "table" then
settings = {}
end
if settings.enabled == nil then settings.enabled = false end
settings.unlocked = settings.unlocked == true
settings.posX = ClampNumber(settings.posX, -2000, 2000, 0)
settings.posY = ClampNumber(settings.posY, -2000, 2000, 120)
settings.width = math.floor(ClampNumber(settings.width, 160, 420, 260) + 0.5)
settings.rowHeight = math.floor(ClampNumber(settings.rowHeight, 18, 42, 24) + 0.5)
settings.iconSize = math.floor(ClampNumber(settings.iconSize, 14, 32, 20) + 0.5)
settings.fontSize = math.floor(ClampNumber(settings.fontSize, 8, 24, 12) + 0.5)
if type(settings.trackedDebuffs) ~= "table" then
settings.trackedDebuffs = {}
return settings
end
local normalized = {}
for sid, value in pairs(settings.trackedDebuffs) do
local id = tonumber(sid)
if id and id > 0 and value ~= false and value ~= nil then
normalized[id] = true
end
end
settings.trackedDebuffs = normalized
return settings
end
local function SetFontObject(fontString, size)
if not fontString or type(fontString.SetFont) ~= "function" then
return
end
local fontPath = STANDARD_TEXT_FONT or "Fonts\\FRIZQT__.TTF"
fontString:SetFont(fontPath, tonumber(size) or 12, "OUTLINE")
end
local function IsAuraDataHarmful(auraData)
if type(auraData) ~= "table" then
return false
end
if auraData.isHarmful ~= nil then
return auraData.isHarmful == true
end
if auraData.isHelpful ~= nil then
return auraData.isHelpful ~= true
end
return true
end
local function GetPlayerTrackedDebuff(spellId)
local sid = tonumber(spellId)
if not sid or sid <= 0 then
return nil
end
if C_UnitAuras and type(C_UnitAuras.GetPlayerAuraBySpellID) == "function" then
local auraData = C_UnitAuras.GetPlayerAuraBySpellID(sid)
if auraData and IsAuraDataHarmful(auraData) then
return {
spellId = sid,
name = tostring(auraData.name or GetSpellName(sid) or ("Spell " .. tostring(sid))),
icon = auraData.icon or auraData.iconFileID or GetSpellIcon(sid),
applications = NormalizeAuraApplications(auraData),
duration = tonumber(auraData.duration) or 0,
expirationTime = tonumber(auraData.expirationTime) or 0,
}
end
end
if AuraUtil and type(AuraUtil.FindAuraBySpellID) == "function" then
local name, icon, applications, _, duration, expirationTime =
AuraUtil.FindAuraBySpellID(sid, "player", "HARMFUL")
if name then
return {
spellId = sid,
name = tostring(name),
icon = icon or GetSpellIcon(sid),
applications = math.max(0, tonumber(applications) or 0),
duration = tonumber(duration) or 0,
expirationTime = tonumber(expirationTime) or 0,
}
end
end
return nil
end
function PA:GetSettings()
local profile = HMGT.db and HMGT.db.profile
if not profile then
return nil
end
profile.personalAuras = NormalizeSettings(profile.personalAuras)
return profile.personalAuras
end
function PA:GetTrackedSpellIds()
local ids = {}
local settings = self:GetSettings()
local tracked = settings and settings.trackedDebuffs or {}
for sid, enabled in pairs(tracked) do
local id = tonumber(sid)
if id and id > 0 and enabled then
ids[#ids + 1] = id
end
end
table.sort(ids, function(a, b)
local nameA = tostring(GetSpellName(a) or a):lower()
local nameB = tostring(GetSpellName(b) or b):lower()
if nameA == nameB then
return a < b
end
return nameA < nameB
end)
return ids
end
function PA:GetTrackedEntries()
local entries = {}
for _, sid in ipairs(self:GetTrackedSpellIds()) do
entries[#entries + 1] = {
spellId = sid,
name = GetSpellName(sid) or ("Spell " .. tostring(sid)),
icon = GetSpellIcon(sid),
}
end
return entries
end
function PA:AddTrackedDebuff(spellId)
local sid = tonumber(spellId)
if not sid or sid <= 0 then
return false, "invalid"
end
local name = GetSpellName(sid)
if not name then
return false, "invalid"
end
local settings = self:GetSettings()
settings.trackedDebuffs[sid] = true
self:Refresh()
return true, name
end
function PA:RemoveTrackedDebuff(spellId)
local sid = tonumber(spellId)
if not sid or sid <= 0 then
return false, "invalid"
end
local settings = self:GetSettings()
if not settings.trackedDebuffs[sid] then
return false, "missing"
end
settings.trackedDebuffs[sid] = nil
self:Refresh()
return true, GetSpellName(sid) or ("Spell " .. tostring(sid))
end
function PA:HasTrackedDebuffsConfigured()
return #self:GetTrackedSpellIds() > 0
end
function PA:GetActiveDebuffEntries()
local now = GetTime()
local entries = {}
for _, sid in ipairs(self:GetTrackedSpellIds()) do
local auraData = GetPlayerTrackedDebuff(sid)
if auraData then
local expirationTime = tonumber(auraData.expirationTime) or 0
local remaining = 0
if expirationTime > 0 then
remaining = math.max(0, expirationTime - now)
end
auraData.remaining = remaining
entries[#entries + 1] = auraData
end
end
table.sort(entries, function(a, b)
local expA = tonumber(a.expirationTime) or 0
local expB = tonumber(b.expirationTime) or 0
if expA > 0 and expB > 0 and expA ~= expB then
return expA < expB
end
if expA > 0 and expB <= 0 then
return true
end
if expB > 0 and expA <= 0 then
return false
end
return tostring(a.name or "") < tostring(b.name or "")
end)
return entries
end
function PA:SaveFramePosition()
local frame = self.frame
local settings = self:GetSettings()
if not frame or not settings then
return
end
local centerX, centerY = frame:GetCenter()
local parentX, parentY = UIParent:GetCenter()
if not centerX or not centerY or not parentX or not parentY then
return
end
settings.posX = math.floor((centerX - parentX) + 0.5)
settings.posY = math.floor((centerY - parentY) + 0.5)
end
function PA:ApplyFramePosition()
local frame = self.frame
local settings = self:GetSettings()
if not frame or not settings then
return
end
frame:ClearAllPoints()
frame:SetPoint("CENTER", UIParent, "CENTER", tonumber(settings.posX) or 0, tonumber(settings.posY) or 120)
end
function PA:AcquireRow(index)
self.rows = self.rows or {}
local row = self.rows[index]
if row then
return row
end
row = CreateFrame("Frame", nil, self.frame)
row:EnableMouse(true)
row.icon = row:CreateTexture(nil, "ARTWORK")
row.name = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
row.timer = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
row.icon:SetPoint("LEFT", row, "LEFT", 8, 0)
row.name:SetPoint("LEFT", row.icon, "RIGHT", 8, 0)
row.name:SetPoint("RIGHT", row.timer, "LEFT", -8, 0)
row.name:SetJustifyH("LEFT")
row.timer:SetPoint("RIGHT", row, "RIGHT", -8, 0)
row.timer:SetJustifyH("RIGHT")
row:SetScript("OnEnter", function(selfRow)
if not selfRow.spellId or not GameTooltip then
return
end
GameTooltip:SetOwner(selfRow, "ANCHOR_RIGHT")
if type(GameTooltip.SetSpellByID) == "function" then
GameTooltip:SetSpellByID(selfRow.spellId)
else
GameTooltip:SetText(GetSpellName(selfRow.spellId) or ("Spell " .. tostring(selfRow.spellId)))
end
if HMGT.SafeShowTooltip then
HMGT:SafeShowTooltip(GameTooltip)
else
GameTooltip:Show()
end
end)
row:SetScript("OnLeave", function()
if GameTooltip then
GameTooltip:Hide()
end
end)
self.rows[index] = row
return row
end
function PA:EnsureFrame()
if self.frame then
return self.frame
end
local frame = CreateFrame("Frame", "HMGTPersonalAurasFrame", UIParent, BackdropTemplateMixin and "BackdropTemplate" or nil)
frame:SetClampedToScreen(true)
frame:SetMovable(true)
frame:EnableMouse(true)
frame:RegisterForDrag("LeftButton")
frame:SetScript("OnDragStart", function(selfFrame)
local settings = self:GetSettings()
if settings and settings.unlocked then
selfFrame:StartMoving()
end
end)
frame:SetScript("OnDragStop", function(selfFrame)
selfFrame:StopMovingOrSizing()
self:SaveFramePosition()
end)
frame:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true,
tileSize = 16,
edgeSize = 12,
insets = { left = 3, right = 3, top = 3, bottom = 3 },
})
frame:SetBackdropColor(0.02, 0.02, 0.04, 0.88)
frame:SetBackdropBorderColor(0.35, 0.35, 0.4, 1)
frame.title = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
frame.title:SetPoint("TOPLEFT", frame, "TOPLEFT", 10, -9)
frame.title:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -10, -9)
frame.title:SetJustifyH("LEFT")
frame.placeholder = frame:CreateFontString(nil, "OVERLAY", "GameFontDisable")
frame.placeholder:SetPoint("CENTER", frame, "CENTER", 0, -4)
frame.placeholder:SetJustifyH("CENTER")
frame.placeholder:SetJustifyV("MIDDLE")
self.frame = frame
self.rows = {}
self:ApplyFramePosition()
return frame
end
function PA:UpdateFrameInteractivity()
local frame = self.frame
local settings = self:GetSettings()
if not frame or not settings then
return
end
frame:EnableMouse(settings.unlocked == true)
if settings.unlocked then
frame:SetBackdropBorderColor(1, 0.82, 0.15, 0.9)
else
frame:SetBackdropBorderColor(0.35, 0.35, 0.4, 1)
end
end
function PA:RenderFrame(entries)
local settings = self:GetSettings()
local frame = self:EnsureFrame()
entries = entries or {}
self:ApplyFramePosition()
self:UpdateFrameInteractivity()
if not settings or settings.enabled ~= true then
frame:Hide()
return
end
local showFrame = (#entries > 0) or settings.unlocked
if not showFrame then
frame:Hide()
return
end
local width = tonumber(settings.width) or 260
local rowHeight = tonumber(settings.rowHeight) or 24
local iconSize = tonumber(settings.iconSize) or 20
local fontSize = tonumber(settings.fontSize) or 12
local headerHeight = 26
local bottomPadding = 8
local visibleRows = math.max(#entries, settings.unlocked and 1 or 0)
local height = headerHeight + bottomPadding + (visibleRows * rowHeight)
if height < 72 then
height = 72
end
frame:SetSize(width, height)
SetFontObject(frame.title, fontSize + 1)
SetFontObject(frame.placeholder, fontSize)
frame.title:SetText((L["PA_NAME"] or "Personal Auras") .. ((#entries > 0) and (" (" .. tostring(#entries) .. ")") or ""))
frame.placeholder:SetText(L["OPT_PA_UNLOCK_HINT"] or "Personal Auras\nDrag to move")
frame.placeholder:SetShown(#entries == 0 and settings.unlocked)
for index, entry in ipairs(entries) do
local row = self:AcquireRow(index)
row:ClearAllPoints()
row:SetPoint("TOPLEFT", frame, "TOPLEFT", 6, -(headerHeight + ((index - 1) * rowHeight)))
row:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -6, -(headerHeight + ((index - 1) * rowHeight)))
row:SetHeight(rowHeight)
row.spellId = tonumber(entry.spellId) or 0
row.icon:SetSize(iconSize, iconSize)
row.icon:SetTexture(entry.icon or GetSpellIcon(entry.spellId))
if (tonumber(entry.applications) or 0) > 1 then
row.name:SetText(string.format("%s x%d", tostring(entry.name or ("Spell " .. tostring(entry.spellId))), tonumber(entry.applications) or 0))
else
row.name:SetText(tostring(entry.name or ("Spell " .. tostring(entry.spellId))))
end
row.timer:SetText(FormatRemainingTime(entry.remaining))
SetFontObject(row.name, fontSize)
SetFontObject(row.timer, fontSize)
row:Show()
end
if self.rows then
for index = #entries + 1, #self.rows do
self.rows[index]:Hide()
end
end
frame:Show()
end
function PA:StopTicker()
if self.ticker then
self.ticker:Cancel()
self.ticker = nil
end
end
function PA:EnsureTicker()
if self.ticker then
return
end
self.ticker = C_Timer.NewTicker(0.1, function()
self:OnTicker()
end)
end
function PA:UpdateRuntimeEventRegistrations()
if not self.eventFrame then
return
end
self.eventFrame:UnregisterEvent("PLAYER_ENTERING_WORLD")
self.eventFrame:UnregisterEvent("UNIT_AURA")
if not self.runtimeEnabled then
return
end
self.eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
local settings = self:GetSettings()
if settings and settings.enabled and self:HasTrackedDebuffsConfigured() then
self.eventFrame:RegisterUnitEvent("UNIT_AURA", "player")
end
end
function PA:Refresh()
if not self.runtimeEnabled then
return
end
self:UpdateRuntimeEventRegistrations()
local settings = self:GetSettings()
if not settings or settings.enabled ~= true then
self:StopTicker()
if self.frame then
self.frame:Hide()
end
return
end
local entries = self:GetActiveDebuffEntries()
self:RenderFrame(entries)
if #entries > 0 then
self:EnsureTicker()
else
self:StopTicker()
end
end
function PA:OnTicker()
if not self.runtimeEnabled then
self:StopTicker()
return
end
local entries = self:GetActiveDebuffEntries()
self:RenderFrame(entries)
if #entries == 0 then
self:StopTicker()
end
end
function PA:OnEvent(event, ...)
if event == "UNIT_AURA" then
local unit = ...
if unit ~= "player" then
return
end
end
self:Refresh()
end
function PA:StartRuntime()
if self.runtimeEnabled then
self:Refresh()
return
end
self.runtimeEnabled = true
if not self.eventFrame then
self.eventFrame = CreateFrame("Frame")
self.eventFrame:SetScript("OnEvent", function(_, event, ...)
self:OnEvent(event, ...)
end)
end
self:Refresh()
end
function PA:StopRuntime()
self.runtimeEnabled = false
if self.eventFrame then
self.eventFrame:UnregisterEvent("PLAYER_ENTERING_WORLD")
self.eventFrame:UnregisterEvent("UNIT_AURA")
end
self:StopTicker()
if self.frame then
self.frame:Hide()
end
end
function PA:OnInitialize()
HMGT.PersonalAuras = self
end
function PA:OnEnable()
self:StartRuntime()
end
function PA:OnDisable()
self:StopRuntime()
end

View File

@@ -0,0 +1,308 @@
local ADDON_NAME = "HailMaryGuildTools"
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
if not HMGT then return end
local PA = HMGT.PersonalAuras
if not PA then return end
if not HMGT_Config or not HMGT_Config.RegisterOptionsProvider then return end
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME, true) or {}
local AceConfigRegistry = LibStub("AceConfigRegistry-3.0", true)
local optionsGroup
local function GetSpellName(spellId)
local sid = tonumber(spellId)
if not sid or sid <= 0 then return nil end
if C_Spell and type(C_Spell.GetSpellName) == "function" then
local name = C_Spell.GetSpellName(sid)
if type(name) == "string" and name ~= "" then
return name
end
end
if type(GetSpellInfo) == "function" then
local name = GetSpellInfo(sid)
if type(name) == "string" and name ~= "" then
return name
end
end
return nil
end
local function GetSpellIcon(spellId)
if HMGT_SpellData and type(HMGT_SpellData.GetSpellIcon) == "function" then
local icon = HMGT_SpellData.GetSpellIcon(tonumber(spellId) or 0)
if icon and icon ~= "" then
return icon
end
end
if C_Spell and type(C_Spell.GetSpellTexture) == "function" then
local icon = C_Spell.GetSpellTexture(tonumber(spellId) or 0)
if icon and icon ~= "" then
return icon
end
end
return 136243
end
local function GetDraft()
HMGT._personalAurasDraft = HMGT._personalAurasDraft or {}
return HMGT._personalAurasDraft
end
local function NotifyOptionsChanged()
if optionsGroup then
local fresh = PA:BuildOptionsGroup()
if type(fresh) == "table" and type(fresh.args) == "table" then
optionsGroup.args = fresh.args
optionsGroup.childGroups = fresh.childGroups
end
end
if AceConfigRegistry and type(AceConfigRegistry.NotifyChange) == "function" then
AceConfigRegistry:NotifyChange(ADDON_NAME)
end
end
local function BuildTrackedLeaf(entry)
local spellId = tonumber(entry and entry.spellId) or 0
return {
type = "group",
name = tostring(entry and entry.name or ("Spell " .. tostring(spellId))),
args = {
header = {
type = "header",
order = 1,
name = function()
local icon = GetSpellIcon(spellId)
local name = tostring(GetSpellName(spellId) or ("Spell " .. tostring(spellId)))
return string.format("|T%s:16:16:0:0|t %s", tostring(icon), name)
end,
},
details = {
type = "description",
order = 2,
width = "full",
name = function()
local isActive = false
for _, activeEntry in ipairs(PA:GetActiveDebuffEntries()) do
if tonumber(activeEntry.spellId) == spellId then
isActive = true
break
end
end
return string.format(
"|cffffd100Spell ID|r: %d\n|cffffd100%s|r: %s",
spellId,
L["OPT_PA_ACTIVE"] or "Active",
isActive and (YES or "Yes") or (NO or "No")
)
end,
},
remove = {
type = "execute",
order = 3,
width = "full",
name = L["OPT_PA_REMOVE"] or "Remove debuff",
func = function()
local ok, info = PA:RemoveTrackedDebuff(spellId)
if ok then
HMGT:Print(string.format(L["OPT_PA_MSG_REMOVED"] or "HMGT: debuff removed: %s", tostring(info or spellId)))
else
HMGT:Print(L["OPT_PA_MSG_NOT_FOUND"] or "HMGT: debuff not found")
end
NotifyOptionsChanged()
end,
},
},
}
end
function PA:BuildOptionsGroup()
local draft = GetDraft()
local entries = self:GetTrackedEntries()
local group = {
type = "group",
name = L["PA_NAME"] or "Personal Auras",
order = 4,
childGroups = "tree",
args = {
general = {
type = "group",
order = 1,
name = L["OPT_PA_SECTION_GENERAL"] or "General",
args = {
enabled = {
type = "toggle",
order = 1,
width = "full",
name = L["OPT_PA_ENABLED"] or "Enable personal auras",
desc = L["OPT_PA_ENABLED_DESC"] or "Show tracked debuffs on your current player in a movable frame",
get = function()
return self:GetSettings().enabled == true
end,
set = function(_, val)
self:GetSettings().enabled = val and true or false
self:Refresh()
end,
},
unlocked = {
type = "toggle",
order = 2,
width = "full",
name = L["OPT_PA_UNLOCK"] or "Unlock frame",
desc = L["OPT_PA_UNLOCK_DESC"] or "Show the frame placeholder and allow it to be moved",
get = function()
return self:GetSettings().unlocked == true
end,
set = function(_, val)
self:GetSettings().unlocked = val and true or false
self:Refresh()
end,
},
width = {
type = "range",
order = 3,
min = 160,
max = 420,
step = 1,
width = "full",
name = L["OPT_PA_WIDTH"] or "Frame width",
get = function()
return tonumber(self:GetSettings().width) or 260
end,
set = function(_, val)
self:GetSettings().width = math.floor((tonumber(val) or 260) + 0.5)
self:Refresh()
end,
},
rowHeight = {
type = "range",
order = 4,
min = 18,
max = 42,
step = 1,
width = "full",
name = L["OPT_PA_ROW_HEIGHT"] or "Row height",
get = function()
return tonumber(self:GetSettings().rowHeight) or 24
end,
set = function(_, val)
self:GetSettings().rowHeight = math.floor((tonumber(val) or 24) + 0.5)
self:Refresh()
end,
},
iconSize = {
type = "range",
order = 5,
min = 14,
max = 32,
step = 1,
width = "full",
name = L["OPT_PA_ICON_SIZE"] or "Icon size",
get = function()
return tonumber(self:GetSettings().iconSize) or 20
end,
set = function(_, val)
self:GetSettings().iconSize = math.floor((tonumber(val) or 20) + 0.5)
self:Refresh()
end,
},
fontSize = {
type = "range",
order = 6,
min = 8,
max = 24,
step = 1,
width = "full",
name = L["OPT_PA_FONT_SIZE"] or "Font size",
get = function()
return tonumber(self:GetSettings().fontSize) or 12
end,
set = function(_, val)
self:GetSettings().fontSize = math.floor((tonumber(val) or 12) + 0.5)
self:Refresh()
end,
},
},
},
tracked = {
type = "group",
order = 2,
name = string.format(
"%s (%d)",
L["OPT_PA_SECTION_DEBUFFS"] or "Tracked debuffs",
#entries
),
args = {
addSpellId = {
type = "input",
order = 1,
width = "full",
name = L["OPT_PA_ADD_ID"] or "Add Spell ID",
get = function()
return tostring(draft.spellId or "")
end,
set = function(_, value)
draft.spellId = tostring(value or "")
end,
},
add = {
type = "execute",
order = 2,
width = "full",
name = L["OPT_PA_ADD"] or "Add debuff",
func = function()
local sid = tonumber(draft.spellId)
local ok, info = self:AddTrackedDebuff(sid)
if ok then
HMGT:Print(string.format(L["OPT_PA_MSG_ADDED"] or "HMGT: debuff added: %s", tostring(info or sid)))
draft.spellId = ""
NotifyOptionsChanged()
else
HMGT:Print(L["OPT_PA_MSG_INVALID"] or "HMGT: invalid debuff spell ID")
end
end,
},
},
},
},
}
if #entries == 0 then
group.args.tracked.args.empty = {
type = "description",
order = 10,
width = "full",
name = L["OPT_PA_EMPTY"] or "No debuffs configured.",
}
else
for index, entry in ipairs(entries) do
local spellId = tonumber(entry.spellId) or index
group.args.tracked.args["debuff_" .. tostring(spellId)] = {
type = "group",
order = 10 + index,
name = function()
return string.format("|T%s:16:16:0:0|t %s", tostring(entry.icon or GetSpellIcon(spellId)), tostring(entry.name or GetSpellName(spellId) or ("Spell " .. tostring(spellId))))
end,
inline = true,
args = BuildTrackedLeaf(entry).args,
}
end
end
return group
end
function PA:GetOptionsGroup()
if not optionsGroup then
optionsGroup = self:BuildOptionsGroup()
end
return optionsGroup
end
HMGT_Config:RegisterOptionsProvider("personalAuras", function()
return {
path = "personalAuras",
order = 4,
group = PA:GetOptionsGroup(),
}
end)

View File

@@ -14,7 +14,8 @@ It combines cooldown tracking, encounter reminders, notes, and map utilities in
- Custom tracker system with individual tracker configs
- Interrupt, raid cooldown, and group cooldown tracking
- Per-tracker bar and icon layouts
- Buff Ending Announcer for selected buffs and channels
- Aura Expiry for selected buffs and channels
- Personal Auras for tracked debuffs on your player
- Raid Timeline for encounter-based text reminders and raid cooldown assignments
- Notes window for raid or personal note management
- Map Overlay with custom world map POIs
@@ -28,10 +29,14 @@ It combines cooldown tracking, encounter reminders, notes, and map utilities in
The tracker system is the core feature of HMGT.
It supports multiple independent tracker bars with custom spell categories, display modes, growth directions, party-frame attachment, and demo/test modes.
### Buff Ending Announcer
### Aura Expiry
Tracks selected buffs and channel-based spells and warns before they expire.
### Personal Auras
Shows selected debuffs on your player in a movable frame.
### Map Overlay
Lets you create custom POIs on the world map and assign curated icon presets.
@@ -83,8 +88,10 @@ Provides a dedicated notes window for raid notes, personal notes, and drafts.
Shared windows and developer tooling
- `Modules/Tracker/`
Tracker rendering, data, and options
- `Modules/BuffEndingAnnouncer/`
Buff ending module and options
- `Modules/AuraExpiry/`
Aura expiry module and options
- `Modules/PersonalAuras/`
Personal debuff watcher module and options
- `Modules/MapOverlay/`
World map POIs, icon config, and options
- `Modules/RaidTimeline/`