Compare commits
15 Commits
7f5bf677d4
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f97b7556cd | ||
|
|
cf78405148 | ||
| feaa62309c | |||
|
|
02e062d66b | ||
|
|
f1d2a761e4 | ||
|
|
258cadeba5 | ||
|
|
8c37da2d38 | ||
|
|
6151b434b1 | ||
|
|
50ff7c93b4 | ||
|
|
7ab82e7655 | ||
|
|
e5d39d88ea | ||
|
|
c3326dde88 | ||
| 5f1c24495c | |||
| 51179a1614 | |||
| 59cbb45f1f |
@@ -12,63 +12,125 @@ jobs:
|
||||
steps:
|
||||
- name: Tools installieren
|
||||
run: |
|
||||
set -e
|
||||
apt-get update
|
||||
apt-get install -y git zip curl jq rsync
|
||||
|
||||
- name: Variablen setzen
|
||||
run: |
|
||||
set -e
|
||||
echo "REPO=${{ gitea.repository }}" >> "$GITHUB_ENV"
|
||||
echo "TAG=${{ gitea.ref_name }}" >> "$GITHUB_ENV"
|
||||
echo "SERVER_URL=https://git.local.unique-studios.de" >> "$GITHUB_ENV"
|
||||
echo "API_BASE=https://git.local.unique-studios.de/api/v1" >> "$GITHUB_ENV"
|
||||
|
||||
- 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
|
||||
set -e
|
||||
rm -rf /tmp/repo /tmp/build /tmp/release.json /tmp/assets.json
|
||||
|
||||
git clone "https://oauth2:${{ secrets.PAT_TOKEN }}@git.local.unique-studios.de/${REPO}.git" /tmp/repo
|
||||
|
||||
- name: ZIP mit Addon-Ordner bauen
|
||||
run: |
|
||||
set -e
|
||||
mkdir -p /tmp/build/HailMaryGuildTools
|
||||
|
||||
REPO_NAME="${REPO##*/}"
|
||||
|
||||
mkdir -p "/tmp/build/${REPO_NAME}"
|
||||
|
||||
rsync -a \
|
||||
--exclude='.git' \
|
||||
--exclude='.gitea' \
|
||||
/tmp/repo/ /tmp/build/HailMaryGuildTools/
|
||||
/tmp/repo/ "/tmp/build/${REPO_NAME}/"
|
||||
|
||||
cd /tmp/build
|
||||
zip -r "/tmp/HailMaryGuildTools-${{ gitea.ref_name }}.zip" HailMaryGuildTools
|
||||
ls -lh "/tmp/HailMaryGuildTools-${{ gitea.ref_name }}.zip"
|
||||
zip -r "/tmp/${REPO_NAME}-${TAG}.zip" "${REPO_NAME}"
|
||||
ls -lh "/tmp/${REPO_NAME}-${TAG}.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"
|
||||
|
||||
API="${API_BASE}/repos/${REPO}"
|
||||
REPO_NAME="${REPO##*/}"
|
||||
|
||||
echo "Server: ${SERVER_URL}"
|
||||
echo "Repo: ${REPO}"
|
||||
echo "Tag: ${TAG}"
|
||||
|
||||
HTTP_CODE=$(curl -s -o /tmp/release.json -w "%{http_code}" \
|
||||
-H "Authorization: token ${{ secrets.PAT_TOKEN }}" \
|
||||
"$API/releases/tags/$TAG")
|
||||
-H "Accept: application/json" \
|
||||
"${API}/releases/tags/${TAG}")
|
||||
|
||||
if [ "$HTTP_CODE" = "200" ]; then
|
||||
echo "Release existiert bereits."
|
||||
else
|
||||
elif [ "$HTTP_CODE" = "404" ]; then
|
||||
echo "Release wird erstellt."
|
||||
curl --fail -s \
|
||||
-X POST \
|
||||
-H "Authorization: token ${{ secrets.PAT_TOKEN }}" \
|
||||
-H "Accept: application/json" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"tag_name\": \"$TAG\",
|
||||
\"name\": \"HailMaryGuildTools $TAG\",
|
||||
\"tag_name\": \"${TAG}\",
|
||||
\"name\": \"${REPO_NAME} ${TAG}\",
|
||||
\"draft\": false,
|
||||
\"prerelease\": false
|
||||
}" \
|
||||
"$API/releases" > /tmp/release.json
|
||||
"${API}/releases" > /tmp/release.json
|
||||
else
|
||||
echo "Unerwarteter HTTP-Code beim Laden des Releases: ${HTTP_CODE}"
|
||||
cat /tmp/release.json || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Release-Daten:"
|
||||
jq '.id, .tag_name, .html_url' /tmp/release.json
|
||||
|
||||
- name: Vorhandenes Asset mit gleichem Namen löschen
|
||||
run: |
|
||||
set -e
|
||||
|
||||
RELEASE_ID=$(jq -r '.id' /tmp/release.json)
|
||||
REPO_NAME="${REPO##*/}"
|
||||
FILE_NAME="${REPO_NAME}-${TAG}.zip"
|
||||
ASSET_API="${API_BASE}/repos/${REPO}/releases/${RELEASE_ID}/assets"
|
||||
|
||||
curl --fail -s \
|
||||
-H "Authorization: token ${{ secrets.PAT_TOKEN }}" \
|
||||
-H "Accept: application/json" \
|
||||
"${ASSET_API}" > /tmp/assets.json
|
||||
|
||||
ASSET_ID=$(jq -r ".[] | select(.name == \"${FILE_NAME}\") | .id" /tmp/assets.json | head -n1)
|
||||
|
||||
if [ -n "$ASSET_ID" ] && [ "$ASSET_ID" != "null" ]; then
|
||||
echo "Vorhandenes Asset gefunden: ${ASSET_ID} -> wird gelöscht"
|
||||
curl --fail -s \
|
||||
-X DELETE \
|
||||
-H "Authorization: token ${{ secrets.PAT_TOKEN }}" \
|
||||
-H "Accept: application/json" \
|
||||
"${ASSET_API}/${ASSET_ID}"
|
||||
else
|
||||
echo "Kein vorhandenes Asset mit gleichem Namen gefunden."
|
||||
fi
|
||||
|
||||
- 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 \
|
||||
RELEASE_ID=$(jq -r '.id' /tmp/release.json)
|
||||
REPO_NAME="${REPO##*/}"
|
||||
FILE="/tmp/${REPO_NAME}-${TAG}.zip"
|
||||
FILE_NAME="$(basename "$FILE")"
|
||||
UPLOAD_URL="${API_BASE}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${FILE_NAME}"
|
||||
|
||||
echo "Upload URL: ${UPLOAD_URL}"
|
||||
|
||||
curl --fail -v \
|
||||
-X POST \
|
||||
-H "Authorization: token ${{ secrets.PAT_TOKEN }}" \
|
||||
-H "Accept: application/json" \
|
||||
-F "attachment=@${FILE}" \
|
||||
"http://10.10.2.140:3000/api/v1/repos/Torsten/HailMaryGuildTools/releases/${RELEASE_ID}/assets?name=$(basename "$FILE")"
|
||||
"${UPLOAD_URL}"
|
||||
@@ -1,486 +0,0 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = _G[ADDON_NAME]
|
||||
if not HMGT then return end
|
||||
|
||||
local L = HMGT.L or LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
|
||||
local AceGUI = LibStub("AceGUI-3.0", true)
|
||||
|
||||
local function GetOrderedDebugLevels()
|
||||
return { "error", "info", "verbose" }
|
||||
end
|
||||
|
||||
local function GetOrderedDebugScopes()
|
||||
local values = HMGT:GetDebugScopeOptions() or {}
|
||||
local names = { "ALL" }
|
||||
for scope in pairs(values) do
|
||||
if scope ~= "ALL" then
|
||||
names[#names + 1] = scope
|
||||
end
|
||||
end
|
||||
table.sort(names, function(a, b)
|
||||
if a == "ALL" then return true end
|
||||
if b == "ALL" then return false end
|
||||
return tostring(values[a] or a) < tostring(values[b] or b)
|
||||
end)
|
||||
return names, values
|
||||
end
|
||||
|
||||
local function SetFilterButtonText(buttonWidget, prefix, valueLabel)
|
||||
if not buttonWidget then
|
||||
return
|
||||
end
|
||||
buttonWidget:SetText(string.format("%s: %s", tostring(prefix or ""), tostring(valueLabel or "")))
|
||||
end
|
||||
|
||||
local function AdvanceDebugLevel(step)
|
||||
local levels = GetOrderedDebugLevels()
|
||||
local current = HMGT:GetConfiguredDebugLevel()
|
||||
local nextIndex = 1
|
||||
for index, value in ipairs(levels) do
|
||||
if value == current then
|
||||
nextIndex = index + (step or 1)
|
||||
break
|
||||
end
|
||||
end
|
||||
if nextIndex < 1 then
|
||||
nextIndex = #levels
|
||||
elseif nextIndex > #levels then
|
||||
nextIndex = 1
|
||||
end
|
||||
HMGT.db.profile.debugLevel = levels[nextIndex]
|
||||
HMGT:RefreshDebugWindow()
|
||||
end
|
||||
|
||||
local function AdvanceDebugScope(step)
|
||||
local scopes, labels = GetOrderedDebugScopes()
|
||||
local current = (HMGT.db and HMGT.db.profile and HMGT.db.profile.debugScope) or "ALL"
|
||||
local nextIndex = 1
|
||||
for index, value in ipairs(scopes) do
|
||||
if value == current then
|
||||
nextIndex = index + (step or 1)
|
||||
break
|
||||
end
|
||||
end
|
||||
if nextIndex < 1 then
|
||||
nextIndex = #scopes
|
||||
elseif nextIndex > #scopes then
|
||||
nextIndex = 1
|
||||
end
|
||||
HMGT.db.profile.debugScope = scopes[nextIndex]
|
||||
HMGT:RefreshDebugWindow()
|
||||
end
|
||||
|
||||
function HMGT:SetDebugWindowMinimized(minimized)
|
||||
local frame = self.debugWindow
|
||||
if not frame then
|
||||
return
|
||||
end
|
||||
|
||||
minimized = minimized and true or false
|
||||
self.debugWindowStatus = self.debugWindowStatus or {
|
||||
width = 860,
|
||||
height = 340,
|
||||
}
|
||||
self.debugWindowStatus.minimized = minimized
|
||||
|
||||
local collapsedHeight = 64
|
||||
if minimized then
|
||||
self.debugWindowStatus.restoreHeight = self.debugWindowStatus.height or frame:GetHeight() or 340
|
||||
end
|
||||
|
||||
local targetHeight = minimized
|
||||
and collapsedHeight
|
||||
or (self.debugWindowStatus.restoreHeight or self.debugWindowStatus.height or 340)
|
||||
|
||||
if frame.aceWidget then
|
||||
frame.aceWidget:EnableResize(not minimized)
|
||||
frame.aceWidget:SetHeight(targetHeight)
|
||||
else
|
||||
frame:SetHeight(targetHeight)
|
||||
end
|
||||
|
||||
if frame.minimizeButton then
|
||||
frame.minimizeButton:SetText(minimized and "+" or "-")
|
||||
end
|
||||
if frame.clearButton then
|
||||
local buttonFrame = frame.clearButton.frame or frame.clearButton
|
||||
buttonFrame:SetShown(not minimized)
|
||||
end
|
||||
if frame.selectButton then
|
||||
local buttonFrame = frame.selectButton.frame or frame.selectButton
|
||||
buttonFrame:SetShown(not minimized)
|
||||
end
|
||||
if frame.levelFilter then
|
||||
local filterFrame = frame.levelFilter.frame or frame.levelFilter
|
||||
filterFrame:SetShown(not minimized)
|
||||
end
|
||||
if frame.scopeFilter then
|
||||
local filterFrame = frame.scopeFilter.frame or frame.scopeFilter
|
||||
filterFrame:SetShown(not minimized)
|
||||
end
|
||||
if frame.logWidget then
|
||||
frame.logWidget.frame:SetShown(not minimized)
|
||||
end
|
||||
if frame.scrollBG then
|
||||
frame.scrollBG:SetShown(not minimized)
|
||||
end
|
||||
|
||||
if not minimized then
|
||||
self:RefreshDebugWindow()
|
||||
end
|
||||
end
|
||||
|
||||
function HMGT:ToggleDebugWindowMinimized()
|
||||
self:SetDebugWindowMinimized(not (self.debugWindowStatus and self.debugWindowStatus.minimized))
|
||||
end
|
||||
|
||||
function HMGT:EnsureDebugWindow()
|
||||
if self.debugWindow then
|
||||
return self.debugWindow
|
||||
end
|
||||
|
||||
local frameWidget
|
||||
if AceGUI then
|
||||
frameWidget = AceGUI:Create("Frame")
|
||||
self.debugWindowStatus = self.debugWindowStatus or {
|
||||
width = 860,
|
||||
height = 340,
|
||||
}
|
||||
frameWidget:SetTitle(L["DEBUG_WINDOW_TITLE"] or "HMGT Debug Console")
|
||||
frameWidget:SetStatusText(L["DEBUG_WINDOW_HINT"] or "Mouse wheel scrolls, Ctrl+A selects all, Ctrl+C copies selected text")
|
||||
frameWidget:SetStatusTable(self.debugWindowStatus)
|
||||
frameWidget:SetWidth(self.debugWindowStatus.width or 860)
|
||||
frameWidget:SetHeight(self.debugWindowStatus.height or 340)
|
||||
frameWidget:EnableResize(true)
|
||||
frameWidget.frame:SetClampedToScreen(true)
|
||||
frameWidget.frame:SetToplevel(true)
|
||||
frameWidget.frame:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
frameWidget:Hide()
|
||||
end
|
||||
|
||||
local frame = frameWidget and frameWidget.frame or CreateFrame("Frame", "HMGT_DebugWindow", UIParent, "BackdropTemplate")
|
||||
if not frameWidget then
|
||||
frame:SetSize(860, 340)
|
||||
frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
|
||||
frame:SetFrameStrata("DIALOG")
|
||||
frame:SetClampedToScreen(true)
|
||||
frame:SetMovable(true)
|
||||
frame:EnableMouse(true)
|
||||
frame:RegisterForDrag("LeftButton")
|
||||
frame:SetScript("OnDragStart", function(selfFrame) selfFrame:StartMoving() end)
|
||||
frame:SetScript("OnDragStop", function(selfFrame) selfFrame:StopMovingOrSizing() end)
|
||||
frame:SetBackdrop({
|
||||
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||||
edgeSize = 12,
|
||||
insets = { left = 3, right = 3, top = 3, bottom = 3 },
|
||||
})
|
||||
frame:SetBackdropColor(0.05, 0.05, 0.06, 0.95)
|
||||
frame:SetBackdropBorderColor(0.35, 0.55, 0.85, 1)
|
||||
frame:Hide()
|
||||
|
||||
local title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
|
||||
title:SetPoint("TOPLEFT", frame, "TOPLEFT", 14, -12)
|
||||
title:SetText(L["DEBUG_WINDOW_TITLE"] or "HMGT Debug Console")
|
||||
frame.title = title
|
||||
|
||||
local closeButton = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
|
||||
closeButton:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -5, -5)
|
||||
frame.closeButton = closeButton
|
||||
|
||||
local minimizeButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
|
||||
minimizeButton:SetSize(22, 20)
|
||||
minimizeButton:SetPoint("TOPRIGHT", closeButton, "TOPLEFT", -2, 0)
|
||||
minimizeButton:SetText((self.debugWindowStatus and self.debugWindowStatus.minimized) and "+" or "-")
|
||||
minimizeButton:SetScript("OnClick", function()
|
||||
HMGT:ToggleDebugWindowMinimized()
|
||||
end)
|
||||
frame.minimizeButton = minimizeButton
|
||||
end
|
||||
|
||||
frame.aceWidget = frameWidget
|
||||
|
||||
if frameWidget and AceGUI then
|
||||
local content = frameWidget.content
|
||||
|
||||
local minimizeButton = AceGUI:Create("Button")
|
||||
minimizeButton:SetText((self.debugWindowStatus and self.debugWindowStatus.minimized) and "+" or "-")
|
||||
minimizeButton:SetWidth(24)
|
||||
minimizeButton:SetCallback("OnClick", function()
|
||||
HMGT:ToggleDebugWindowMinimized()
|
||||
end)
|
||||
minimizeButton.frame:SetParent(frame)
|
||||
minimizeButton.frame:ClearAllPoints()
|
||||
minimizeButton.frame:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -34, -4)
|
||||
minimizeButton.frame:SetHeight(20)
|
||||
minimizeButton.frame:Show()
|
||||
frame.minimizeButton = minimizeButton
|
||||
|
||||
local clearButton = AceGUI:Create("Button")
|
||||
clearButton:SetText(L["OPT_DEBUG_CLEAR"] or "Clear log")
|
||||
clearButton:SetWidth(120)
|
||||
clearButton:SetCallback("OnClick", function()
|
||||
HMGT:ClearDebugLog()
|
||||
end)
|
||||
clearButton.frame:SetParent(content)
|
||||
clearButton.frame:ClearAllPoints()
|
||||
clearButton.frame:SetPoint("TOPRIGHT", content, "TOPRIGHT", 0, -2)
|
||||
clearButton.frame:Show()
|
||||
frame.clearButton = clearButton
|
||||
|
||||
local selectButton = AceGUI:Create("Button")
|
||||
selectButton:SetText(L["OPT_DEBUG_SELECT_ALL"] or "Select all")
|
||||
selectButton:SetWidth(120)
|
||||
selectButton:SetCallback("OnClick", function()
|
||||
if frame.editBox then
|
||||
frame.editBox:SetFocus()
|
||||
frame.editBox:HighlightText(0)
|
||||
end
|
||||
end)
|
||||
selectButton.frame:SetParent(content)
|
||||
selectButton.frame:ClearAllPoints()
|
||||
selectButton.frame:SetPoint("TOPRIGHT", clearButton.frame, "TOPLEFT", -6, 0)
|
||||
selectButton.frame:Show()
|
||||
frame.selectButton = selectButton
|
||||
|
||||
local levelFilter = AceGUI:Create("Button")
|
||||
levelFilter:SetWidth(150)
|
||||
levelFilter:SetCallback("OnClick", function()
|
||||
AdvanceDebugLevel(1)
|
||||
end)
|
||||
levelFilter.frame:SetParent(content)
|
||||
levelFilter.frame:ClearAllPoints()
|
||||
levelFilter.frame:SetPoint("TOPLEFT", content, "TOPLEFT", 0, 0)
|
||||
levelFilter.frame:Show()
|
||||
frame.levelFilter = levelFilter
|
||||
|
||||
local scopeFilter = AceGUI:Create("Button")
|
||||
scopeFilter:SetWidth(180)
|
||||
scopeFilter:SetCallback("OnClick", function()
|
||||
AdvanceDebugScope(1)
|
||||
end)
|
||||
scopeFilter.frame:SetParent(content)
|
||||
scopeFilter.frame:ClearAllPoints()
|
||||
scopeFilter.frame:SetPoint("TOPLEFT", levelFilter.frame, "TOPRIGHT", 8, 0)
|
||||
scopeFilter.frame:Show()
|
||||
frame.scopeFilter = scopeFilter
|
||||
|
||||
local logWidget = AceGUI:Create("MultiLineEditBox")
|
||||
logWidget:SetLabel("")
|
||||
logWidget:DisableButton(true)
|
||||
logWidget:SetNumLines(18)
|
||||
logWidget:SetText("")
|
||||
logWidget.frame:SetParent(content)
|
||||
logWidget.frame:ClearAllPoints()
|
||||
logWidget.frame:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -54)
|
||||
logWidget.frame:SetPoint("BOTTOMRIGHT", content, "BOTTOMRIGHT", 0, 0)
|
||||
logWidget.frame:Show()
|
||||
logWidget:SetCallback("OnTextChanged", function()
|
||||
HMGT:RefreshDebugWindow()
|
||||
end)
|
||||
logWidget.editBox:SetScript("OnKeyDown", function(selfBox, key)
|
||||
if IsControlKeyDown() and (key == "A" or key == "a") then
|
||||
selfBox:HighlightText(0)
|
||||
end
|
||||
end)
|
||||
frame.logWidget = logWidget
|
||||
frame.editBox = logWidget.editBox
|
||||
frame.scrollFrame = logWidget.scrollFrame
|
||||
|
||||
self.debugWindow = frame
|
||||
self:SetDebugWindowMinimized(self.debugWindowStatus and self.debugWindowStatus.minimized)
|
||||
return frame
|
||||
end
|
||||
|
||||
local clearButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
|
||||
clearButton:SetSize(90, 22)
|
||||
clearButton:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -30, -6)
|
||||
clearButton:SetText(L["OPT_DEBUG_CLEAR"] or "Clear log")
|
||||
clearButton:SetScript("OnClick", function()
|
||||
HMGT:ClearDebugLog()
|
||||
end)
|
||||
frame.clearButton = clearButton
|
||||
|
||||
local selectButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
|
||||
selectButton:SetSize(90, 22)
|
||||
selectButton:SetPoint("TOPRIGHT", clearButton, "TOPLEFT", -6, 0)
|
||||
selectButton:SetText(L["OPT_DEBUG_SELECT_ALL"] or "Select all")
|
||||
selectButton:SetScript("OnClick", function()
|
||||
if frame.editBox then
|
||||
frame.editBox:SetFocus()
|
||||
frame.editBox:HighlightText(0)
|
||||
end
|
||||
end)
|
||||
frame.selectButton = selectButton
|
||||
|
||||
local scopeFilter = CreateFrame("Frame", nil, frame)
|
||||
scopeFilter:SetSize(170, 22)
|
||||
scopeFilter:SetPoint("TOPLEFT", frame, "TOPLEFT", 16, -8)
|
||||
frame.scopeFilter = scopeFilter
|
||||
|
||||
local scrollBG = CreateFrame("Frame", nil, frame, "BackdropTemplate")
|
||||
scrollBG:SetBackdrop({
|
||||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||||
edgeSize = 16,
|
||||
insets = { left = 4, right = 3, top = 4, bottom = 3 },
|
||||
})
|
||||
scrollBG:SetBackdropColor(0, 0, 0, 0.95)
|
||||
scrollBG:SetBackdropBorderColor(0.4, 0.4, 0.4, 1)
|
||||
scrollBG:SetPoint("TOPLEFT", frame, "TOPLEFT", 14, -36)
|
||||
scrollBG:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -30, 14)
|
||||
frame.scrollBG = scrollBG
|
||||
|
||||
local scrollFrame = CreateFrame("ScrollFrame", nil, scrollBG, "UIPanelScrollFrameTemplate")
|
||||
scrollFrame:SetPoint("TOPLEFT", scrollBG, "TOPLEFT", 6, -6)
|
||||
scrollFrame:SetPoint("BOTTOMRIGHT", scrollBG, "BOTTOMRIGHT", -27, 4)
|
||||
scrollFrame:EnableMouseWheel(true)
|
||||
scrollFrame:SetScript("OnMouseWheel", function(selfMsg, delta)
|
||||
if delta > 0 then
|
||||
selfMsg:SetVerticalScroll(math.max(0, selfMsg:GetVerticalScroll() - 42))
|
||||
else
|
||||
selfMsg:SetVerticalScroll(selfMsg:GetVerticalScroll() + 42)
|
||||
end
|
||||
end)
|
||||
frame.scrollFrame = scrollFrame
|
||||
|
||||
local editBox = CreateFrame("EditBox", nil, scrollFrame)
|
||||
editBox:SetMultiLine(true)
|
||||
editBox:SetAutoFocus(false)
|
||||
editBox:SetFontObject(ChatFontNormal)
|
||||
editBox:SetWidth(780)
|
||||
editBox:SetTextInsets(6, 6, 6, 6)
|
||||
editBox:EnableMouse(true)
|
||||
editBox:SetScript("OnEscapePressed", function(selfBox)
|
||||
selfBox:ClearFocus()
|
||||
end)
|
||||
editBox:SetScript("OnKeyDown", function(selfBox, key)
|
||||
if IsControlKeyDown() and (key == "A" or key == "a") then
|
||||
selfBox:HighlightText(0)
|
||||
end
|
||||
end)
|
||||
editBox:SetScript("OnTextChanged", function(selfBox, userInput)
|
||||
if userInput then
|
||||
HMGT:RefreshDebugWindow()
|
||||
else
|
||||
selfBox:SetCursorPosition(selfBox:GetNumLetters())
|
||||
selfBox:SetHeight(math.max(scrollFrame:GetHeight(), HMGT:GetDebugWindowTextHeight(frame, selfBox:GetText()) + 16))
|
||||
scrollFrame:UpdateScrollChildRect()
|
||||
end
|
||||
end)
|
||||
editBox:SetScript("OnMouseUp", function(selfBox)
|
||||
selfBox:SetFocus()
|
||||
end)
|
||||
scrollFrame:SetScrollChild(editBox)
|
||||
frame.editBox = editBox
|
||||
|
||||
local measureText = frame:CreateFontString(nil, "ARTWORK", "ChatFontNormal")
|
||||
measureText:SetJustifyH("LEFT")
|
||||
measureText:SetJustifyV("TOP")
|
||||
if measureText.SetSpacing then
|
||||
measureText:SetSpacing(2)
|
||||
end
|
||||
measureText:SetWidth(768)
|
||||
frame.measureText = measureText
|
||||
|
||||
self.debugWindow = frame
|
||||
self:SetDebugWindowMinimized(self.debugWindowStatus and self.debugWindowStatus.minimized)
|
||||
return frame
|
||||
end
|
||||
|
||||
function HMGT:GetDebugWindowTextHeight(frame, text)
|
||||
if not frame or not frame.measureText then
|
||||
return 0
|
||||
end
|
||||
|
||||
local width = 768
|
||||
if frame.editBox then
|
||||
width = math.max(1, (frame.editBox:GetWidth() or width) - 12)
|
||||
end
|
||||
frame.measureText:SetWidth(width)
|
||||
frame.measureText:SetText(text or "")
|
||||
return frame.measureText:GetStringHeight()
|
||||
end
|
||||
|
||||
function HMGT:RefreshDebugWindow()
|
||||
local frame = self:EnsureDebugWindow()
|
||||
if not frame then
|
||||
return
|
||||
end
|
||||
|
||||
local filtered = self:GetFilteredDebugBuffer() or self.debugBuffer or {}
|
||||
local text = table.concat(filtered, "\n")
|
||||
if frame.logWidget and frame.editBox then
|
||||
if frame.levelFilter then
|
||||
local levelOptions = self:GetDebugLevelOptions()
|
||||
SetFilterButtonText(frame.levelFilter, L["OPT_DEBUG_LEVEL"] or "Level", levelOptions[self:GetConfiguredDebugLevel()])
|
||||
end
|
||||
if frame.scopeFilter then
|
||||
local scopeOptions = self:GetDebugScopeOptions()
|
||||
local currentScope = (self.db and self.db.profile and self.db.profile.debugScope) or "ALL"
|
||||
SetFilterButtonText(frame.scopeFilter, L["OPT_DEBUG_SCOPE"] or "Module", scopeOptions[currentScope] or currentScope)
|
||||
end
|
||||
frame.logWidget:SetText(text)
|
||||
frame.editBox:SetCursorPosition(frame.editBox:GetNumLetters())
|
||||
return
|
||||
end
|
||||
|
||||
if not frame.editBox then
|
||||
return
|
||||
end
|
||||
|
||||
frame.editBox:SetText(text)
|
||||
frame.editBox:SetCursorPosition(#text)
|
||||
frame.editBox:SetHeight(math.max(frame.scrollFrame:GetHeight(), self:GetDebugWindowTextHeight(frame, text) + 16))
|
||||
frame.scrollFrame:SetVerticalScroll(math.max(0, frame.editBox:GetHeight() - frame.scrollFrame:GetHeight()))
|
||||
end
|
||||
|
||||
function HMGT:UpdateDebugWindowVisibility()
|
||||
if self.db and self.db.profile then
|
||||
self.db.profile.debug = false
|
||||
end
|
||||
local frame = self.debugWindow
|
||||
if not frame then
|
||||
return
|
||||
end
|
||||
local widget = frame.aceWidget
|
||||
if widget then
|
||||
widget:Hide()
|
||||
else
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
function HMGT:ClearDebugLog()
|
||||
wipe(self.debugBuffer)
|
||||
if self.debugWindow and self.debugWindow.logWidget then
|
||||
self.debugWindow.logWidget:SetText("")
|
||||
self.debugWindow.editBox:SetCursorPosition(0)
|
||||
self.debugWindow.scrollFrame:SetVerticalScroll(0)
|
||||
return
|
||||
end
|
||||
if self.debugWindow and self.debugWindow.editBox then
|
||||
self.debugWindow.editBox:SetText("")
|
||||
self.debugWindow.scrollFrame:SetVerticalScroll(0)
|
||||
end
|
||||
end
|
||||
|
||||
function HMGT:ToggleDebugWindowShortcut()
|
||||
if self.db and self.db.profile then
|
||||
self.db.profile.debug = false
|
||||
end
|
||||
local frame = self.debugWindow
|
||||
if not frame then
|
||||
return
|
||||
end
|
||||
local widget = frame.aceWidget
|
||||
if widget then
|
||||
widget:Hide()
|
||||
else
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
function HMGT:DumpDebugLog(maxLines)
|
||||
return
|
||||
end
|
||||
@@ -5,7 +5,7 @@ if not HMGT then return end
|
||||
local L = HMGT.L or LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
|
||||
|
||||
HMGT.devToolsBuffer = HMGT.devToolsBuffer or {}
|
||||
HMGT.devToolsBufferMax = HMGT.devToolsBufferMax or 300
|
||||
HMGT.devToolsBufferMax = HMGT.devToolsBufferMax or 500
|
||||
|
||||
local DEVTOOLS_SCOPE_ALL = "ALL"
|
||||
local DEVTOOLS_SCOPE_LABELS = {
|
||||
@@ -20,7 +20,8 @@ local DEVTOOLS_SCOPE_LABELS = {
|
||||
|
||||
local DEVTOOLS_LEVELS = {
|
||||
error = 1,
|
||||
trace = 2,
|
||||
info = 2,
|
||||
verbose = 3,
|
||||
}
|
||||
|
||||
local function TrimText(value)
|
||||
@@ -76,8 +77,10 @@ function HMGT:GetDevToolsSettings()
|
||||
profile.devTools = type(profile.devTools) == "table" and profile.devTools or {}
|
||||
local settings = profile.devTools
|
||||
settings.enabled = settings.enabled == true
|
||||
if settings.level ~= "error" and settings.level ~= "trace" then
|
||||
settings.level = "error"
|
||||
if settings.level == "trace" then
|
||||
settings.level = "verbose"
|
||||
elseif settings.level ~= "error" and settings.level ~= "info" and settings.level ~= "verbose" then
|
||||
settings.level = "info"
|
||||
end
|
||||
if type(settings.scope) ~= "string" or settings.scope == "" then
|
||||
settings.scope = DEVTOOLS_SCOPE_ALL
|
||||
@@ -94,24 +97,25 @@ function HMGT:IsDevToolsEnabled()
|
||||
end
|
||||
|
||||
function HMGT:GetDevToolsLevelOptions()
|
||||
return {
|
||||
error = L["OPT_DEVTOOLS_LEVEL_ERROR"] or "Errors",
|
||||
trace = L["OPT_DEVTOOLS_LEVEL_TRACE"] or "Trace",
|
||||
}
|
||||
return self:GetDebugLevelOptions()
|
||||
end
|
||||
|
||||
function HMGT:GetConfiguredDevToolsLevel()
|
||||
return self:GetDevToolsSettings().level or "error"
|
||||
return self:GetConfiguredDebugLevel()
|
||||
end
|
||||
|
||||
function HMGT:ShouldIncludeDevToolsLevel(level)
|
||||
local configured = self:GetConfiguredDevToolsLevel()
|
||||
return (DEVTOOLS_LEVELS[tostring(level or "error")] or DEVTOOLS_LEVELS.error)
|
||||
<= (DEVTOOLS_LEVELS[configured] or DEVTOOLS_LEVELS.error)
|
||||
local normalizedLevel = tostring(level or "info")
|
||||
if normalizedLevel == "trace" then
|
||||
normalizedLevel = "verbose"
|
||||
end
|
||||
return (DEVTOOLS_LEVELS[normalizedLevel] or DEVTOOLS_LEVELS.info)
|
||||
<= (DEVTOOLS_LEVELS[configured] or DEVTOOLS_LEVELS.info)
|
||||
end
|
||||
|
||||
function HMGT:GetDevToolsScopeOptions()
|
||||
local values = {
|
||||
local values = self:GetDebugScopeOptions() or {
|
||||
[DEVTOOLS_SCOPE_ALL] = L["OPT_DEVTOOLS_SCOPE_ALL"] or "All scopes",
|
||||
}
|
||||
for scope, label in pairs(DEVTOOLS_SCOPE_LABELS) do
|
||||
@@ -128,8 +132,11 @@ end
|
||||
|
||||
function HMGT:FormatDevToolsEntry(entry)
|
||||
local stamp = tostring(entry and entry.stamp or date("%H:%M:%S"))
|
||||
local level = string.upper(tostring(entry and entry.level or "error"))
|
||||
local level = string.upper(tostring(entry and entry.level or "info"))
|
||||
local scope = tostring(entry and entry.scope or "System")
|
||||
if entry and entry.kind == "debug" then
|
||||
return string.format("%s [%s][%s] %s", stamp, level, scope, tostring(entry.message or ""))
|
||||
end
|
||||
local eventName = tostring(entry and entry.event or "")
|
||||
local payload = TrimText(entry and entry.payload or "")
|
||||
if payload ~= "" then
|
||||
@@ -164,8 +171,10 @@ function HMGT:RecordDevEvent(level, scope, eventName, payload)
|
||||
end
|
||||
|
||||
local normalizedLevel = tostring(level or "error")
|
||||
if normalizedLevel ~= "error" and normalizedLevel ~= "trace" then
|
||||
normalizedLevel = "trace"
|
||||
if normalizedLevel == "trace" then
|
||||
normalizedLevel = "verbose"
|
||||
elseif normalizedLevel ~= "error" and normalizedLevel ~= "info" and normalizedLevel ~= "verbose" then
|
||||
normalizedLevel = "verbose"
|
||||
end
|
||||
if not self:ShouldIncludeDevToolsLevel(normalizedLevel) then
|
||||
return
|
||||
@@ -182,6 +191,7 @@ function HMGT:RecordDevEvent(level, scope, eventName, payload)
|
||||
scope = normalizedScope,
|
||||
event = TrimText(eventName or "event"),
|
||||
payload = EncodePayloadValue(payload, 0),
|
||||
kind = "event",
|
||||
}
|
||||
|
||||
table.insert(self.devToolsBuffer, entry)
|
||||
@@ -194,6 +204,40 @@ function HMGT:RecordDevEvent(level, scope, eventName, payload)
|
||||
end
|
||||
end
|
||||
|
||||
function HMGT:RecordDebugEntry(level, scope, message)
|
||||
if not self:IsDevToolsEnabled() then
|
||||
return
|
||||
end
|
||||
|
||||
local normalizedLevel = tostring(level or "info")
|
||||
if normalizedLevel == "trace" then
|
||||
normalizedLevel = "verbose"
|
||||
elseif normalizedLevel ~= "error" and normalizedLevel ~= "info" and normalizedLevel ~= "verbose" then
|
||||
normalizedLevel = "info"
|
||||
end
|
||||
|
||||
local normalizedScope = TrimText(scope or "General")
|
||||
if normalizedScope == "" then
|
||||
normalizedScope = "General"
|
||||
end
|
||||
|
||||
self.devToolsBuffer = self.devToolsBuffer or {}
|
||||
self.devToolsBuffer[#self.devToolsBuffer + 1] = {
|
||||
stamp = date("%H:%M:%S"),
|
||||
level = normalizedLevel,
|
||||
scope = normalizedScope,
|
||||
message = TrimText(message or ""),
|
||||
kind = "debug",
|
||||
}
|
||||
while #self.devToolsBuffer > (tonumber(self.devToolsBufferMax) or 500) do
|
||||
table.remove(self.devToolsBuffer, 1)
|
||||
end
|
||||
|
||||
if self.devToolsWindow and self.devToolsWindow:IsShown() and self.RefreshDevToolsWindow then
|
||||
self:RefreshDevToolsWindow()
|
||||
end
|
||||
end
|
||||
|
||||
function HMGT:DevError(scope, eventName, payload)
|
||||
self:RecordDevEvent("error", scope, eventName, payload)
|
||||
end
|
||||
|
||||
@@ -7,7 +7,7 @@ local AceGUI = LibStub("AceGUI-3.0", true)
|
||||
if not AceGUI then return end
|
||||
|
||||
local function GetOrderedLevels()
|
||||
return { "error", "trace" }
|
||||
return { "error", "info", "verbose" }
|
||||
end
|
||||
|
||||
local function GetOrderedScopes()
|
||||
@@ -78,8 +78,8 @@ function HMGT:EnsureDevToolsWindow()
|
||||
|
||||
local settings = self:GetDevToolsSettings()
|
||||
local window = self:CreateAceWindow("devTools", {
|
||||
title = L["DEVTOOLS_WINDOW_TITLE"] or "HMGT Developer Tools",
|
||||
statusText = L["DEVTOOLS_WINDOW_HINT"] or "Structured developer events for the current session",
|
||||
title = L["DEVTOOLS_WINDOW_TITLE"] or "HMGT Debug Console",
|
||||
statusText = L["DEVTOOLS_WINDOW_HINT"] or "Debug and developer events for the current session",
|
||||
statusTable = settings.window,
|
||||
width = settings.window.width or 920,
|
||||
height = settings.window.height or 420,
|
||||
@@ -93,7 +93,7 @@ function HMGT:EnsureDevToolsWindow()
|
||||
local content = window:GetContent()
|
||||
|
||||
local clearButton = AceGUI:Create("Button")
|
||||
clearButton:SetText(L["OPT_DEVTOOLS_CLEAR"] or "Clear developer log")
|
||||
clearButton:SetText(L["OPT_DEVTOOLS_CLEAR"] or L["OPT_DEBUG_CLEAR"] or "Clear log")
|
||||
clearButton:SetWidth(140)
|
||||
clearButton:SetCallback("OnClick", function()
|
||||
HMGT:ClearDevToolsLog()
|
||||
@@ -176,11 +176,11 @@ function HMGT:RefreshDevToolsWindow()
|
||||
end
|
||||
|
||||
local levelOptions = self:GetDevToolsLevelOptions()
|
||||
SetFilterButtonText(window.levelFilter, L["OPT_DEVTOOLS_LEVEL"] or "Capture level", levelOptions[self:GetConfiguredDevToolsLevel()])
|
||||
SetFilterButtonText(window.levelFilter, L["OPT_DEBUG_LEVEL"] or L["OPT_DEVTOOLS_LEVEL"] or "Level", levelOptions[self:GetConfiguredDevToolsLevel()])
|
||||
|
||||
local scopeValues = self:GetDevToolsScopeOptions()
|
||||
local currentScope = self:GetDevToolsSettings().scope or "ALL"
|
||||
SetFilterButtonText(window.scopeFilter, L["OPT_DEVTOOLS_SCOPE"] or "Scope", scopeValues[currentScope] or currentScope)
|
||||
SetFilterButtonText(window.scopeFilter, L["OPT_DEBUG_SCOPE"] or L["OPT_DEVTOOLS_SCOPE"] or "Module", scopeValues[currentScope] or currentScope)
|
||||
|
||||
local text = table.concat(self:GetFilteredDevToolsLines(), "\n")
|
||||
window.logWidget:SetText(text)
|
||||
|
||||
@@ -3,6 +3,218 @@ local HMGT = _G[ADDON_NAME]
|
||||
if not HMGT then return end
|
||||
|
||||
local L = HMGT.L or LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
|
||||
local AceGUI = LibStub("AceGUI-3.0", true)
|
||||
local CLASS_ICON_TCOORDS = CLASS_ICON_TCOORDS or {}
|
||||
|
||||
local function NormalizeName(name)
|
||||
if HMGT.NormalizePlayerName then
|
||||
return HMGT:NormalizePlayerName(name)
|
||||
end
|
||||
return tostring(name or "")
|
||||
end
|
||||
|
||||
local function GetRosterRows()
|
||||
local rows = {}
|
||||
local seen = {}
|
||||
|
||||
local function addUnit(unitId)
|
||||
if not unitId or not UnitExists(unitId) then
|
||||
return
|
||||
end
|
||||
local name = NormalizeName(UnitName(unitId))
|
||||
if not name or name == "" or seen[name] then
|
||||
return
|
||||
end
|
||||
seen[name] = true
|
||||
rows[#rows + 1] = {
|
||||
name = name,
|
||||
class = select(2, UnitClass(unitId)),
|
||||
isLeader = UnitIsGroupLeader and UnitIsGroupLeader(unitId) or false,
|
||||
isAssistant = UnitIsGroupAssistant and UnitIsGroupAssistant(unitId) or false,
|
||||
connected = UnitIsConnected and UnitIsConnected(unitId) ~= false or true,
|
||||
isPlayer = UnitIsUnit and UnitIsUnit(unitId, "player") or unitId == "player",
|
||||
}
|
||||
end
|
||||
|
||||
if IsInRaid() then
|
||||
for i = 1, GetNumGroupMembers() do
|
||||
addUnit("raid" .. i)
|
||||
end
|
||||
elseif IsInGroup() then
|
||||
addUnit("player")
|
||||
for i = 1, GetNumSubgroupMembers() do
|
||||
addUnit("party" .. i)
|
||||
end
|
||||
else
|
||||
addUnit("player")
|
||||
end
|
||||
|
||||
table.sort(rows, function(a, b)
|
||||
if a.isLeader ~= b.isLeader then
|
||||
return a.isLeader
|
||||
end
|
||||
if a.isPlayer ~= b.isPlayer then
|
||||
return a.isPlayer
|
||||
end
|
||||
return tostring(a.name or "") < tostring(b.name or "")
|
||||
end)
|
||||
|
||||
return rows
|
||||
end
|
||||
|
||||
local function GetPlayerVersionText(name)
|
||||
local normalized = NormalizeName(name)
|
||||
if normalized == NormalizeName(UnitName("player")) then
|
||||
return tostring(HMGT.ADDON_VERSION or "dev"), tonumber(HMGT.PROTOCOL_VERSION) or 0, true
|
||||
end
|
||||
|
||||
local addonStatus = HMGT.GetPlayerAddonStatus and HMGT:GetPlayerAddonStatus(normalized) or nil
|
||||
if addonStatus and addonStatus.mode == "hmgt" and addonStatus.version and addonStatus.version ~= "" then
|
||||
return tostring(addonStatus.version), tonumber(addonStatus.protocol) or 0, true
|
||||
end
|
||||
if addonStatus and addonStatus.mode == "bridge" then
|
||||
return L["VERSION_WINDOW_BRIDGE_MODE"] or "Bridge Mode", 0, true
|
||||
end
|
||||
return nil, tonumber(addonStatus and addonStatus.protocol) or 0, false
|
||||
end
|
||||
|
||||
local function ApplyClassIcon(texture, classTag)
|
||||
if not texture then
|
||||
return
|
||||
end
|
||||
|
||||
local coords = classTag and CLASS_ICON_TCOORDS[classTag]
|
||||
if coords then
|
||||
texture:SetTexture("Interface\\GLUES\\CHARACTERCREATE\\UI-CHARACTERCREATE-CLASSES")
|
||||
texture:SetTexCoord(coords[1], coords[2], coords[3], coords[4])
|
||||
texture:Show()
|
||||
else
|
||||
texture:SetTexture(nil)
|
||||
texture:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
local function AcquireVersionRow(window, index)
|
||||
window.versionRows = window.versionRows or {}
|
||||
local row = window.versionRows[index]
|
||||
if row then
|
||||
return row
|
||||
end
|
||||
|
||||
local parent = window.scrollChild
|
||||
row = CreateFrame("Frame", nil, parent)
|
||||
row:SetHeight(22)
|
||||
|
||||
row.background = row:CreateTexture(nil, "BACKGROUND")
|
||||
row.background:SetAllPoints(row)
|
||||
row.background:SetColorTexture(1, 1, 1, 0.03)
|
||||
|
||||
row.classIcon = row:CreateTexture(nil, "ARTWORK")
|
||||
row.classIcon:SetSize(16, 16)
|
||||
row.classIcon:SetPoint("LEFT", row, "LEFT", 4, 0)
|
||||
|
||||
row.nameText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
|
||||
row.nameText:SetPoint("LEFT", row.classIcon, "RIGHT", 6, 0)
|
||||
row.nameText:SetJustifyH("LEFT")
|
||||
|
||||
row.versionText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
|
||||
row.versionText:SetPoint("LEFT", row, "LEFT", 250, 0)
|
||||
row.versionText:SetWidth(150)
|
||||
row.versionText:SetJustifyH("LEFT")
|
||||
|
||||
row.protocolText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
|
||||
row.protocolText:SetPoint("LEFT", row, "LEFT", 410, 0)
|
||||
row.protocolText:SetWidth(100)
|
||||
row.protocolText:SetJustifyH("LEFT")
|
||||
|
||||
window.versionRows[index] = row
|
||||
return row
|
||||
end
|
||||
|
||||
function HMGT:RefreshVersionNoticeWindow()
|
||||
local window = self.versionNoticeWindow
|
||||
if not window then
|
||||
return
|
||||
end
|
||||
|
||||
local roster = GetRosterRows()
|
||||
local localName = NormalizeName(UnitName("player"))
|
||||
|
||||
for index, info in ipairs(roster) do
|
||||
local row = AcquireVersionRow(window, index)
|
||||
row:ClearAllPoints()
|
||||
if index == 1 then
|
||||
row:SetPoint("TOPLEFT", window.scrollChild, "TOPLEFT", 0, 0)
|
||||
row:SetPoint("TOPRIGHT", window.scrollChild, "TOPRIGHT", 0, 0)
|
||||
else
|
||||
row:SetPoint("TOPLEFT", window.versionRows[index - 1], "BOTTOMLEFT", 0, -2)
|
||||
row:SetPoint("TOPRIGHT", window.versionRows[index - 1], "BOTTOMRIGHT", 0, -2)
|
||||
end
|
||||
|
||||
ApplyClassIcon(row.classIcon, info.class)
|
||||
|
||||
local nameLabel = tostring(info.name or UNKNOWN)
|
||||
if info.isLeader then
|
||||
nameLabel = string.format("%s %s", nameLabel, L["VERSION_WINDOW_LEADER_TAG"] or "(Leader)")
|
||||
elseif info.isAssistant then
|
||||
nameLabel = string.format("%s %s", nameLabel, L["VERSION_WINDOW_ASSISTANT_TAG"] or "(Assist)")
|
||||
end
|
||||
if info.isPlayer or info.name == localName then
|
||||
nameLabel = string.format("%s %s", nameLabel, L["VERSION_WINDOW_SELF_TAG"] or "(You)")
|
||||
end
|
||||
|
||||
row.nameText:SetText(nameLabel)
|
||||
row.nameText:SetTextColor(1, 0.82, 0.1, 1)
|
||||
|
||||
local versionText, protocol, hasAddon = GetPlayerVersionText(info.name)
|
||||
if hasAddon then
|
||||
row.versionText:SetText(versionText or "?")
|
||||
if versionText == (L["VERSION_WINDOW_BRIDGE_MODE"] or "Bridge Mode") then
|
||||
row.versionText:SetTextColor(0.55, 0.82, 1, 1)
|
||||
else
|
||||
row.versionText:SetTextColor(0.9, 0.9, 0.9, 1)
|
||||
end
|
||||
row.protocolText:SetText(protocol > 0 and tostring(protocol) or "-")
|
||||
row.protocolText:SetTextColor(0.75, 0.75, 0.75, 1)
|
||||
else
|
||||
row.versionText:SetText(L["VERSION_WINDOW_MISSING_ADDON"] or "Addon not installed")
|
||||
row.versionText:SetTextColor(1, 0.25, 0.25, 1)
|
||||
row.protocolText:SetText("-")
|
||||
row.protocolText:SetTextColor(1, 0.25, 0.25, 1)
|
||||
end
|
||||
|
||||
row:Show()
|
||||
end
|
||||
|
||||
if window.versionRows then
|
||||
for index = #roster + 1, #window.versionRows do
|
||||
window.versionRows[index]:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
local contentHeight = math.max(1, (#roster * 24))
|
||||
window.scrollChild:SetHeight(contentHeight)
|
||||
|
||||
local known = 0
|
||||
for _, info in ipairs(roster) do
|
||||
local _, _, hasAddon = GetPlayerVersionText(info.name)
|
||||
if hasAddon then
|
||||
known = known + 1
|
||||
end
|
||||
end
|
||||
|
||||
window.messageText:SetText(L["VERSION_WINDOW_MESSAGE"] or "Hail Mary Guild Tools versions in your current group")
|
||||
window.detailText:SetText(string.format(
|
||||
L["VERSION_WINDOW_CURRENT"] or "Current version: %s | Protocol: %s",
|
||||
tostring(HMGT.ADDON_VERSION or "dev"),
|
||||
tostring(HMGT.PROTOCOL_VERSION or "?")
|
||||
))
|
||||
window:SetStatusText(string.format(
|
||||
L["VERSION_WINDOW_STATUS"] or "Detected HMGT on %d/%d players",
|
||||
tonumber(known) or 0,
|
||||
tonumber(#roster) or 0
|
||||
))
|
||||
end
|
||||
|
||||
function HMGT:EnsureVersionNoticeWindow()
|
||||
if self.versionNoticeWindow then
|
||||
@@ -10,21 +222,21 @@ function HMGT:EnsureVersionNoticeWindow()
|
||||
end
|
||||
|
||||
self.versionNoticeWindowStatus = self.versionNoticeWindowStatus or {
|
||||
width = 560,
|
||||
height = 240,
|
||||
width = 640,
|
||||
height = 420,
|
||||
}
|
||||
|
||||
local window = self:CreateAceWindow("versionNotice", {
|
||||
title = L["VERSION_WINDOW_TITLE"] or "HMGT Version Check",
|
||||
statusText = "",
|
||||
statusTable = self.versionNoticeWindowStatus,
|
||||
width = self.versionNoticeWindowStatus.width or 560,
|
||||
height = self.versionNoticeWindowStatus.height or 240,
|
||||
width = self.versionNoticeWindowStatus.width or 640,
|
||||
height = self.versionNoticeWindowStatus.height or 420,
|
||||
backgroundTexture = "Interface\\AddOns\\HailMaryGuildTools\\Media\\HailMaryLogo.png",
|
||||
backgroundWidth = 220,
|
||||
backgroundHeight = 120,
|
||||
backgroundOffsetY = -8,
|
||||
backgroundAlpha = 0.12,
|
||||
backgroundAlpha = 0.08,
|
||||
strata = "FULLSCREEN_DIALOG",
|
||||
})
|
||||
if not window then
|
||||
@@ -32,26 +244,64 @@ function HMGT:EnsureVersionNoticeWindow()
|
||||
end
|
||||
|
||||
local content = window:GetContent()
|
||||
|
||||
local messageText = content:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
|
||||
messageText:SetPoint("TOPLEFT", content, "TOPLEFT", 28, -28)
|
||||
messageText:SetPoint("TOPRIGHT", content, "TOPRIGHT", -28, -28)
|
||||
messageText:SetJustifyH("CENTER")
|
||||
messageText:SetJustifyV("MIDDLE")
|
||||
messageText:SetPoint("TOPLEFT", content, "TOPLEFT", 24, -22)
|
||||
messageText:SetPoint("TOPRIGHT", content, "TOPRIGHT", -24, -22)
|
||||
messageText:SetJustifyH("LEFT")
|
||||
messageText:SetTextColor(1, 0.82, 0.1, 1)
|
||||
messageText:SetText(L["VERSION_WINDOW_MESSAGE"] or "A new version of Hail Mary Guild Tools is available.")
|
||||
window.messageText = messageText
|
||||
|
||||
local detailText = content:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
|
||||
detailText:SetPoint("TOPLEFT", messageText, "BOTTOMLEFT", 0, -18)
|
||||
detailText:SetPoint("TOPRIGHT", messageText, "BOTTOMRIGHT", 0, -18)
|
||||
detailText:SetJustifyH("CENTER")
|
||||
detailText:SetJustifyV("TOP")
|
||||
if detailText.SetSpacing then
|
||||
detailText:SetSpacing(2)
|
||||
end
|
||||
detailText:SetPoint("TOPLEFT", messageText, "BOTTOMLEFT", 0, -8)
|
||||
detailText:SetPoint("TOPRIGHT", messageText, "BOTTOMRIGHT", 0, -8)
|
||||
detailText:SetJustifyH("LEFT")
|
||||
detailText:SetTextColor(0.9, 0.9, 0.9, 1)
|
||||
window.detailText = detailText
|
||||
|
||||
local refreshButton = AceGUI and AceGUI:Create("Button") or nil
|
||||
if refreshButton then
|
||||
refreshButton:SetText(L["VERSION_WINDOW_REFRESH"] or "Refresh")
|
||||
refreshButton:SetWidth(120)
|
||||
refreshButton:SetCallback("OnClick", function()
|
||||
HMGT:RequestSync("VersionWindow")
|
||||
HMGT:RefreshVersionNoticeWindow()
|
||||
end)
|
||||
refreshButton.frame:SetParent(window.frame)
|
||||
refreshButton.frame:ClearAllPoints()
|
||||
refreshButton.frame:SetPoint("TOPRIGHT", content, "TOPRIGHT", -24, -68)
|
||||
refreshButton.frame:Show()
|
||||
window.refreshButton = refreshButton
|
||||
end
|
||||
|
||||
local header = CreateFrame("Frame", nil, content)
|
||||
header:SetPoint("TOPLEFT", detailText, "BOTTOMLEFT", 0, -14)
|
||||
header:SetPoint("TOPRIGHT", content, "TOPRIGHT", -24, -96)
|
||||
header:SetHeight(18)
|
||||
window.header = header
|
||||
|
||||
local nameHeader = header:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
||||
nameHeader:SetPoint("LEFT", header, "LEFT", 24, 0)
|
||||
nameHeader:SetText(L["VERSION_WINDOW_COLUMN_PLAYER"] or "Player")
|
||||
|
||||
local versionHeader = header:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
||||
versionHeader:SetPoint("LEFT", header, "LEFT", 250, 0)
|
||||
versionHeader:SetText(L["VERSION_WINDOW_COLUMN_VERSION"] or "Version")
|
||||
|
||||
local protocolHeader = header:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
||||
protocolHeader:SetPoint("LEFT", header, "LEFT", 410, 0)
|
||||
protocolHeader:SetText(L["VERSION_WINDOW_COLUMN_PROTOCOL"] or "Protocol")
|
||||
|
||||
local scrollFrame = CreateFrame("ScrollFrame", nil, content, "UIPanelScrollFrameTemplate")
|
||||
scrollFrame:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 0, -6)
|
||||
scrollFrame:SetPoint("BOTTOMRIGHT", content, "BOTTOMRIGHT", -28, 24)
|
||||
window.scrollFrame = scrollFrame
|
||||
|
||||
local scrollChild = CreateFrame("Frame", nil, scrollFrame)
|
||||
scrollChild:SetSize(1, 1)
|
||||
scrollFrame:SetScrollChild(scrollChild)
|
||||
window.scrollChild = scrollChild
|
||||
|
||||
self.versionNoticeWindow = window
|
||||
return window
|
||||
end
|
||||
@@ -67,36 +317,16 @@ function HMGT:ShowVersionMismatchPopup(playerName, detail, sourceTag, opts)
|
||||
}
|
||||
end
|
||||
|
||||
local info = self.latestVersionMismatch or {}
|
||||
local window = self:EnsureVersionNoticeWindow()
|
||||
if not window then
|
||||
return
|
||||
end
|
||||
local hasMismatch = info.playerName or info.detail
|
||||
|
||||
window:SetTitle(L["VERSION_WINDOW_TITLE"] or "HMGT Version Check")
|
||||
|
||||
if hasMismatch then
|
||||
window.messageText:SetText(L["VERSION_WINDOW_MESSAGE"] or "A new version of Hail Mary Guild Tools is available.")
|
||||
window.detailText:SetText(string.format(
|
||||
L["VERSION_WINDOW_DETAIL"] or "Detected via %s from %s.\n%s",
|
||||
tostring(info.sourceTag or "?"),
|
||||
tostring(info.playerName or UNKNOWN),
|
||||
tostring(info.detail or "")
|
||||
))
|
||||
else
|
||||
window.messageText:SetText(L["VERSION_WINDOW_NO_MISMATCH"] or "No newer HMGT version has been detected in your current group.")
|
||||
window.detailText:SetText(string.format(
|
||||
L["VERSION_WINDOW_CURRENT"] or "Current version: %s | Protocol: %s",
|
||||
tostring(HMGT.ADDON_VERSION or "dev"),
|
||||
tostring(HMGT.PROTOCOL_VERSION or "?")
|
||||
))
|
||||
end
|
||||
|
||||
self:DevTrace("Version", hasMismatch and "window_show_mismatch" or "window_show_current", {
|
||||
player = info.playerName,
|
||||
source = info.sourceTag,
|
||||
detail = info.detail,
|
||||
self:RefreshVersionNoticeWindow()
|
||||
self:DevTrace("Version", "window_show", {
|
||||
player = playerName,
|
||||
source = sourceTag,
|
||||
detail = detail,
|
||||
})
|
||||
window:Show()
|
||||
window:Raise()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
## Interface: 120000,120001
|
||||
## Interface: 120000,120001,120005
|
||||
## IconTexture: Interface\Addons\HailMaryGuildTools\Media\HailMaryIcon.png
|
||||
## Author: Torsten Brendgen
|
||||
## Title: Hail Mary Guild Tools
|
||||
@@ -31,10 +31,20 @@ HailMaryGuildToolsOptions.lua
|
||||
# ────── Tracker ──────────────────────────────────────────────────────
|
||||
Modules\Tracker\Frame.lua
|
||||
Modules\Tracker\SpellDatabase.lua
|
||||
Modules\Tracker\SingleFrameTrackerBase.lua
|
||||
Modules\Tracker\TrackerCore.lua
|
||||
Modules\Tracker\TrackerState.lua
|
||||
Modules\Tracker\TrackerPlayerState.lua
|
||||
Modules\Tracker\TrackerBridge.lua
|
||||
Modules\Tracker\TrackerDataProvider.lua
|
||||
Modules\Tracker\TrackerSync.lua
|
||||
Modules\Tracker\TrackerAvailability.lua
|
||||
Modules\Tracker\TrackerDetection.lua
|
||||
Modules\Tracker\InterruptTracker\InterruptTracker.lua
|
||||
Modules\Tracker\RaidCooldownTracker\RaidCooldownTracker.lua
|
||||
Modules\Tracker\GroupCooldownTracker\GroupCooldownTracker.lua
|
||||
|
||||
Modules\Tracker\InterruptTracker\InterruptSpellDatabase.lua
|
||||
Modules\Tracker\RaidcooldownTracker\RaidCooldownSpellDatabase.lua
|
||||
Modules\Tracker\RaidCooldownTracker\RaidCooldownSpellDatabase.lua
|
||||
Modules\Tracker\GroupCooldownTracker\GroupCooldownSpellDatabase.lua
|
||||
Modules\Tracker\TrackerManager.lua
|
||||
Modules\Tracker\NormalTrackerFrames.lua
|
||||
@@ -55,5 +65,9 @@ Modules\MapOverlay\MapOverlay.xml
|
||||
Modules\RaidTimeline\RaidTimelineBossAbilityData.lua
|
||||
Modules\RaidTimeline\RaidTimeline.lua
|
||||
Modules\RaidTimeline\RaidTimelineBigWigs.lua
|
||||
Modules\RaidTimeline\RaidTimelineDBM.lua
|
||||
Modules\RaidTimeline\RaidTimelineOptions.lua
|
||||
|
||||
# EncounterAlerts
|
||||
Modules\EncounterAlerts\EncounterAlerts.lua
|
||||
Modules\EncounterAlerts\LuraRunes.lua
|
||||
Modules\EncounterAlerts\EncounterAlertsOptions.lua
|
||||
|
||||
@@ -17,6 +17,9 @@ function HMGT_Config:RegisterOptionsProvider(id, provider)
|
||||
if type(id) ~= "string" or id == "" then return false end
|
||||
if type(provider) ~= "function" then return false end
|
||||
self._optionProviders[id] = provider
|
||||
if type(self.RebuildRootOptions) == "function" then
|
||||
self:RebuildRootOptions()
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -1835,6 +1838,88 @@ function HMGT_Config:Initialize()
|
||||
end
|
||||
end,
|
||||
},
|
||||
devToolsEnabled = {
|
||||
type = "toggle",
|
||||
order = 2,
|
||||
width = "full",
|
||||
name = L["OPT_DEVTOOLS_MODE"] or L["OPT_DEBUG_MODE"] or "Debug console",
|
||||
desc = L["OPT_DEVTOOLS_MODE_DESC"] or L["OPT_DEBUG_MODE_DESC"] or "Enable the debug console.",
|
||||
get = function()
|
||||
return HMGT.GetDevToolsSettings and HMGT:GetDevToolsSettings().enabled == true
|
||||
end,
|
||||
set = function(_, value)
|
||||
if not HMGT.GetDevToolsSettings then
|
||||
return
|
||||
end
|
||||
HMGT:GetDevToolsSettings().enabled = value == true
|
||||
if HMGT.UpdateDevToolsWindowVisibility then
|
||||
HMGT:UpdateDevToolsWindowVisibility()
|
||||
end
|
||||
end,
|
||||
},
|
||||
debugLevel = {
|
||||
type = "select",
|
||||
order = 3,
|
||||
width = "full",
|
||||
name = L["OPT_DEBUG_LEVEL"] or "Debug level",
|
||||
values = function()
|
||||
return HMGT.GetDebugLevelOptions and HMGT:GetDebugLevelOptions() or {}
|
||||
end,
|
||||
get = function()
|
||||
return HMGT.GetConfiguredDebugLevel and HMGT:GetConfiguredDebugLevel() or "info"
|
||||
end,
|
||||
set = function(_, value)
|
||||
if HMGT.GetDevToolsSettings then
|
||||
HMGT:GetDevToolsSettings().level = value or "info"
|
||||
end
|
||||
if HMGT.RefreshDevToolsWindow then
|
||||
HMGT:RefreshDevToolsWindow()
|
||||
end
|
||||
end,
|
||||
},
|
||||
debugScope = {
|
||||
type = "select",
|
||||
order = 4,
|
||||
width = "full",
|
||||
name = L["OPT_DEBUG_SCOPE"] or "Module filter",
|
||||
values = function()
|
||||
return HMGT.GetDebugScopeOptions and HMGT:GetDebugScopeOptions() or {}
|
||||
end,
|
||||
get = function()
|
||||
local settings = HMGT.GetDevToolsSettings and HMGT:GetDevToolsSettings() or {}
|
||||
return settings.scope or "ALL"
|
||||
end,
|
||||
set = function(_, value)
|
||||
if HMGT.GetDevToolsSettings then
|
||||
HMGT:GetDevToolsSettings().scope = value or "ALL"
|
||||
end
|
||||
if HMGT.RefreshDevToolsWindow then
|
||||
HMGT:RefreshDevToolsWindow()
|
||||
end
|
||||
end,
|
||||
},
|
||||
openDebug = {
|
||||
type = "execute",
|
||||
order = 5,
|
||||
width = "half",
|
||||
name = L["OPT_DEVTOOLS_OPEN"] or L["OPT_DEBUG_OPEN"] or "Open debug console",
|
||||
func = function()
|
||||
if HMGT.OpenDevToolsWindow then
|
||||
HMGT:OpenDevToolsWindow()
|
||||
end
|
||||
end,
|
||||
},
|
||||
clearDebug = {
|
||||
type = "execute",
|
||||
order = 6,
|
||||
width = "half",
|
||||
name = L["OPT_DEVTOOLS_CLEAR"] or L["OPT_DEBUG_CLEAR"] or "Clear debug log",
|
||||
func = function()
|
||||
if HMGT.ClearDevToolsLog then
|
||||
HMGT:ClearDevToolsLog()
|
||||
end
|
||||
end,
|
||||
},
|
||||
},
|
||||
},
|
||||
commands = {
|
||||
@@ -1850,6 +1935,8 @@ function HMGT_Config:Initialize()
|
||||
name = table.concat({
|
||||
"|cffffd100/hmgt|r",
|
||||
"|cffffd100/hmgt debug|r",
|
||||
"|cffffd100/hmgt status|r",
|
||||
"|cffffd100/hmgt lura|r",
|
||||
"|cffffd100/hmgt version|r",
|
||||
}, "\n"),
|
||||
},
|
||||
@@ -2000,6 +2087,15 @@ function HMGT_Config:Initialize()
|
||||
modulesGroup.args.raidTimeline = raidTimelineGroup
|
||||
end
|
||||
|
||||
local encounterAlertsGroup = BuildNamedModuleGroup(
|
||||
"encounterAlerts",
|
||||
L["OPT_MODULE_ENCOUNTER_ALERTS"] or "Encounter Alerts",
|
||||
50
|
||||
)
|
||||
if encounterAlertsGroup then
|
||||
modulesGroup.args.encounterAlerts = encounterAlertsGroup
|
||||
end
|
||||
|
||||
if next(modulesGroup.args) == nil then
|
||||
return nil
|
||||
end
|
||||
@@ -2037,12 +2133,20 @@ function HMGT_Config:Initialize()
|
||||
},
|
||||
}
|
||||
|
||||
local modulesGroup = BuildModulesGroup()
|
||||
if modulesGroup then
|
||||
rootOptions.args.modules = modulesGroup
|
||||
function HMGT_Config:RebuildRootOptions()
|
||||
local modulesGroup = BuildModulesGroup()
|
||||
if modulesGroup then
|
||||
rootOptions.args.modules = modulesGroup
|
||||
else
|
||||
rootOptions.args.modules = nil
|
||||
end
|
||||
NormalizeExecuteButtonWidths(rootOptions)
|
||||
if AceConfigRegistry and type(AceConfigRegistry.NotifyChange) == "function" then
|
||||
AceConfigRegistry:NotifyChange(ADDON_NAME)
|
||||
end
|
||||
end
|
||||
|
||||
NormalizeExecuteButtonWidths(rootOptions)
|
||||
HMGT_Config:RebuildRootOptions()
|
||||
|
||||
local aceConfig = LibStub("AceConfig-3.0")
|
||||
local aceConfigDialog = LibStub("AceConfigDialog-3.0")
|
||||
|
||||
@@ -18,12 +18,23 @@ L["SLASH_HINT"] = "/hmgt – Optionen | /hmgt lock/unlock | /hmgt dem
|
||||
L["VERSION_MISMATCH_CHAT"] = "Versionskonflikt mit %s: %s"
|
||||
L["VERSION_MISMATCH_POPUP"] = "HailMaryGuildTools Konflikt mit %s.\n%s\nQuelle: %s"
|
||||
L["VERSION_WINDOW_TITLE"] = "HMGT Versionscheck"
|
||||
L["VERSION_WINDOW_MESSAGE"] = "Es gibt eine neue Version von Hail Mary Guild Tools."
|
||||
L["VERSION_WINDOW_MESSAGE"] = "Hail Mary Guild Tools Versionen in deiner aktuellen Gruppe"
|
||||
L["VERSION_WINDOW_DETAIL"] = "Erkannt ueber %s von %s.\n%s"
|
||||
L["VERSION_WINDOW_NO_MISMATCH"] = "In deiner aktuellen Gruppe wurde keine neuere HMGT-Version erkannt."
|
||||
L["VERSION_WINDOW_CURRENT"] = "Aktuelle Version: %s | Protokoll: %s"
|
||||
L["VERSION_WINDOW_STATUS"] = "Addon oder Bridge bei %d/%d Spielern erkannt"
|
||||
L["VERSION_WINDOW_REFRESH"] = "Aktualisieren"
|
||||
L["VERSION_WINDOW_COLUMN_PLAYER"] = "Spieler"
|
||||
L["VERSION_WINDOW_COLUMN_VERSION"] = "Version"
|
||||
L["VERSION_WINDOW_COLUMN_PROTOCOL"] = "Protokoll"
|
||||
L["VERSION_WINDOW_BRIDGE_MODE"] = "Bridge Mode"
|
||||
L["VERSION_WINDOW_MISSING_ADDON"] = "Addon nicht vorhanden"
|
||||
L["VERSION_WINDOW_LEADER_TAG"] = "(Leiter)"
|
||||
L["VERSION_WINDOW_ASSISTANT_TAG"] = "(Assist)"
|
||||
L["VERSION_WINDOW_SELF_TAG"] = "(Du)"
|
||||
L["VERSION_OUTDATED_WHISPER"] = "Deine Hail Mary Guild Tools Version ist veraltet. Du hast %s, der Gruppenleiter hat %s."
|
||||
L["VERSION_WINDOW_DEBUG_ONLY"] = "HMGT: /hmgt version ist nur bei aktiviertem Debugmodus verfuegbar."
|
||||
L["VERSION_WINDOW_DEVTOOLS_ONLY"] = "HMGT: /hmgt version ist nur bei aktivierten Entwicklerwerkzeugen verfuegbar."
|
||||
L["VERSION_WINDOW_DEVTOOLS_ONLY"] = "HMGT: /hmgt version ist nur bei aktivierter Debug-Konsole verfuegbar."
|
||||
|
||||
-- ── Options: general ─────────────────────────────────────────
|
||||
L["OPT_GENERAL"] = "Allgemein"
|
||||
@@ -53,17 +64,47 @@ L["OPT_DEBUG_CLEAR"] = "Debug-Log leeren"
|
||||
L["OPT_DEBUG_SELECT_ALL"] = "Alles markieren"
|
||||
L["DEBUG_WINDOW_TITLE"] = "HMGT Debug-Konsole"
|
||||
L["DEBUG_WINDOW_HINT"] = "Mit dem Mausrad scrollen, Strg+A markiert alles, Strg+C kopiert markierten Text"
|
||||
L["OPT_DEVTOOLS_MODE"] = "Entwicklerwerkzeuge"
|
||||
L["OPT_DEVTOOLS_MODE_DESC"] = "Aktiviert die strukturierte Entwickler-Konsole."
|
||||
L["OPT_DEVTOOLS_LEVEL"] = "Erfassungsstufe"
|
||||
L["OPT_DEVTOOLS_MODE"] = "Debug-Konsole"
|
||||
L["OPT_DEVTOOLS_MODE_DESC"] = "Aktiviert das gemeinsame Debug- und Entwickler-Log."
|
||||
L["OPT_DEVTOOLS_LEVEL"] = "Debug-Stufe"
|
||||
L["OPT_DEVTOOLS_LEVEL_ERROR"] = "Fehler"
|
||||
L["OPT_DEVTOOLS_LEVEL_TRACE"] = "Trace"
|
||||
L["OPT_DEVTOOLS_SCOPE"] = "Scope-Filter"
|
||||
L["OPT_DEVTOOLS_SCOPE_ALL"] = "Alle Scopes"
|
||||
L["OPT_DEVTOOLS_OPEN"] = "Entwickler-Konsole oeffnen"
|
||||
L["OPT_DEVTOOLS_CLEAR"] = "Entwickler-Log leeren"
|
||||
L["OPT_DEVTOOLS_LEVEL_TRACE"] = "Ausfuehrlich"
|
||||
L["OPT_DEVTOOLS_SCOPE"] = "Modulfilter"
|
||||
L["OPT_DEVTOOLS_SCOPE_ALL"] = "Alle Module"
|
||||
L["OPT_DEVTOOLS_OPEN"] = "Debug-Konsole oeffnen"
|
||||
L["OPT_DEVTOOLS_CLEAR"] = "Debug-Log leeren"
|
||||
L["OPT_DEVTOOLS_SELECT_ALL"] = "Alles markieren"
|
||||
L["OPT_DEVTOOLS_DISABLED"] = "HMGT: Entwicklerwerkzeuge sind nicht aktiviert."
|
||||
L["OPT_MODULE_ENCOUNTER_ALERTS"] = "Encounter Alerts"
|
||||
L["OPT_ENCOUNTER_ALERTS_PLACEHOLDER"] = "Encounter-spezifische Helper-Frames und Warnungen."
|
||||
L["OPT_EA_LURA_TITLE"] = "L'ura Runen"
|
||||
L["OPT_EA_LURA_RUNE_WINDOW"] = "Runen-Fenster"
|
||||
L["OPT_EA_LURA_ENABLED"] = "L'ura Runen aktivieren"
|
||||
L["OPT_EA_LURA_UNLOCK"] = "Runen-Frame entsperren"
|
||||
L["OPT_EA_LURA_HINT"] = "Erste Version: nur Normal/Heroisch Layout. Tank steht unten mittig zwischen Slot 1 und 5."
|
||||
L["OPT_EA_LURA_SHOW"] = "Anzeigen"
|
||||
L["OPT_EA_LURA_TEST"] = "Testmuster"
|
||||
L["OPT_EA_LURA_CLEAR"] = "Leeren"
|
||||
L["OPT_EA_LURA_BROADCAST"] = "Sequenz senden"
|
||||
L["OPT_EA_LURA_ACTIONBAR"] = "Runen-Actionbar"
|
||||
L["OPT_EA_LURA_ACTIONBAR_SHOW"] = "Leiste anzeigen"
|
||||
L["OPT_EA_LURA_ACTIONBAR_UNLOCK"] = "Leiste entsperren"
|
||||
L["OPT_EA_LURA_ACTIONBAR_AUTO_SHOW"] = "Automatisch im Bossraum anzeigen"
|
||||
L["OPT_EA_LURA_ACTIONBAR_ORIENTATION"] = "Ausrichtung"
|
||||
L["OPT_EA_LURA_ACTIONBAR_HORIZONTAL"] = "Horizontal"
|
||||
L["OPT_EA_LURA_ACTIONBAR_VERTICAL"] = "Vertikal"
|
||||
L["OPT_EA_LURA_ACTIONBAR_HINT"] = "Klicke die Runen in beobachteter Reihenfolge. Slot 5 sendet die Sequenz automatisch. Der rote Button leert die lokale Sequenz."
|
||||
L["OPT_EA_LURA_ICON_SIZE"] = "Icongroesse"
|
||||
L["OPT_EA_LURA_BACKGROUND_ALPHA"] = "Hintergrund-Alpha"
|
||||
L["OPT_EA_LURA_ICON_SPACING"] = "Icon-Abstand"
|
||||
L["OPT_EA_LURA_BORDER_ENABLED"] = "Rahmen anzeigen"
|
||||
L["OPT_EA_LURA_BORDER_WIDTH"] = "Rahmenbreite"
|
||||
L["OPT_EA_LURA_BORDER_COLOR"] = "Rahmenfarbe"
|
||||
L["OPT_EA_LURA_SHOW_LABELS"] = "Labels anzeigen"
|
||||
L["OPT_EA_LURA_RUNE_EMPTY"] = "Leer"
|
||||
L["OPT_EA_LURA_DRAG_HINT"] = "Ziehen zum Verschieben"
|
||||
L["OPT_EA_LURA_BOSS"] = "Boss"
|
||||
L["OPT_EA_LURA_TANK"] = "Tank"
|
||||
L["DEVTOOLS_WINDOW_TITLE"] = "HMGT Entwicklerwerkzeuge"
|
||||
L["DEVTOOLS_WINDOW_HINT"] = "Strukturierte Entwickler-Ereignisse fuer die aktuelle Sitzung"
|
||||
L["OPT_SYNC_REMOTE_CHARGES"] = "Remote-Aufladungen synchronisieren"
|
||||
|
||||
@@ -18,12 +18,23 @@ L["SLASH_HINT"] = "/hmgt – options | /hmgt lock/unlock | /hmgt demo
|
||||
L["VERSION_MISMATCH_CHAT"] = "Version mismatch with %s: %s"
|
||||
L["VERSION_MISMATCH_POPUP"] = "HailMaryGuildTools mismatch with %s.\n%s\nSource: %s"
|
||||
L["VERSION_WINDOW_TITLE"] = "HMGT Version Check"
|
||||
L["VERSION_WINDOW_MESSAGE"] = "A new version of Hail Mary Guild Tools is available."
|
||||
L["VERSION_WINDOW_MESSAGE"] = "Hail Mary Guild Tools versions in your current group"
|
||||
L["VERSION_WINDOW_DETAIL"] = "Detected via %s from %s.\n%s"
|
||||
L["VERSION_WINDOW_NO_MISMATCH"] = "No newer HMGT version has been detected in your current group."
|
||||
L["VERSION_WINDOW_CURRENT"] = "Current version: %s | Protocol: %s"
|
||||
L["VERSION_WINDOW_STATUS"] = "Detected addon or bridge on %d/%d players"
|
||||
L["VERSION_WINDOW_REFRESH"] = "Refresh"
|
||||
L["VERSION_WINDOW_COLUMN_PLAYER"] = "Player"
|
||||
L["VERSION_WINDOW_COLUMN_VERSION"] = "Version"
|
||||
L["VERSION_WINDOW_COLUMN_PROTOCOL"] = "Protocol"
|
||||
L["VERSION_WINDOW_BRIDGE_MODE"] = "Bridge Mode"
|
||||
L["VERSION_WINDOW_MISSING_ADDON"] = "Addon not installed"
|
||||
L["VERSION_WINDOW_LEADER_TAG"] = "(Leader)"
|
||||
L["VERSION_WINDOW_ASSISTANT_TAG"] = "(Assist)"
|
||||
L["VERSION_WINDOW_SELF_TAG"] = "(You)"
|
||||
L["VERSION_OUTDATED_WHISPER"] = "Your Hail Mary Guild Tools version is outdated. You have %s, the group leader has %s."
|
||||
L["VERSION_WINDOW_DEBUG_ONLY"] = "HMGT: /hmgt version is only available while debug mode is enabled."
|
||||
L["VERSION_WINDOW_DEVTOOLS_ONLY"] = "HMGT: /hmgt version is only available while developer tools are enabled."
|
||||
L["VERSION_WINDOW_DEVTOOLS_ONLY"] = "HMGT: /hmgt version is only available while the debug console is enabled."
|
||||
|
||||
-- ── Options: general ─────────────────────────────────────────
|
||||
L["OPT_GENERAL"] = "General"
|
||||
@@ -53,17 +64,47 @@ L["OPT_DEBUG_CLEAR"] = "Clear debug log"
|
||||
L["OPT_DEBUG_SELECT_ALL"] = "Select all"
|
||||
L["DEBUG_WINDOW_TITLE"] = "HMGT Debug Console"
|
||||
L["DEBUG_WINDOW_HINT"] = "Mouse wheel scrolls, Ctrl+A selects all, Ctrl+C copies selected text"
|
||||
L["OPT_DEVTOOLS_MODE"] = "Developer tools"
|
||||
L["OPT_DEVTOOLS_MODE_DESC"] = "Enable the structured developer event console."
|
||||
L["OPT_DEVTOOLS_LEVEL"] = "Capture level"
|
||||
L["OPT_DEVTOOLS_MODE"] = "Debug console"
|
||||
L["OPT_DEVTOOLS_MODE_DESC"] = "Enable the shared debug and developer log."
|
||||
L["OPT_DEVTOOLS_LEVEL"] = "Debug level"
|
||||
L["OPT_DEVTOOLS_LEVEL_ERROR"] = "Errors"
|
||||
L["OPT_DEVTOOLS_LEVEL_TRACE"] = "Trace"
|
||||
L["OPT_DEVTOOLS_SCOPE"] = "Scope filter"
|
||||
L["OPT_DEVTOOLS_SCOPE_ALL"] = "All scopes"
|
||||
L["OPT_DEVTOOLS_OPEN"] = "Open developer console"
|
||||
L["OPT_DEVTOOLS_CLEAR"] = "Clear developer log"
|
||||
L["OPT_DEVTOOLS_LEVEL_TRACE"] = "Verbose"
|
||||
L["OPT_DEVTOOLS_SCOPE"] = "Module filter"
|
||||
L["OPT_DEVTOOLS_SCOPE_ALL"] = "All modules"
|
||||
L["OPT_DEVTOOLS_OPEN"] = "Open debug console"
|
||||
L["OPT_DEVTOOLS_CLEAR"] = "Clear debug log"
|
||||
L["OPT_DEVTOOLS_SELECT_ALL"] = "Select all"
|
||||
L["OPT_DEVTOOLS_DISABLED"] = "HMGT: developer tools are not enabled."
|
||||
L["OPT_MODULE_ENCOUNTER_ALERTS"] = "Encounter Alerts"
|
||||
L["OPT_ENCOUNTER_ALERTS_PLACEHOLDER"] = "Encounter-specific helper frames and alerts."
|
||||
L["OPT_EA_LURA_TITLE"] = "L'ura Runes"
|
||||
L["OPT_EA_LURA_RUNE_WINDOW"] = "Rune window"
|
||||
L["OPT_EA_LURA_ENABLED"] = "Enable L'ura runes"
|
||||
L["OPT_EA_LURA_UNLOCK"] = "Unlock rune frame"
|
||||
L["OPT_EA_LURA_HINT"] = "First version: normal/heroic layout only. Tank reference is placed bottom-center between slot 1 and 5."
|
||||
L["OPT_EA_LURA_SHOW"] = "Show"
|
||||
L["OPT_EA_LURA_TEST"] = "Test pattern"
|
||||
L["OPT_EA_LURA_CLEAR"] = "Clear"
|
||||
L["OPT_EA_LURA_BROADCAST"] = "Send sequence"
|
||||
L["OPT_EA_LURA_ACTIONBAR"] = "Rune action bar"
|
||||
L["OPT_EA_LURA_ACTIONBAR_SHOW"] = "Show bar"
|
||||
L["OPT_EA_LURA_ACTIONBAR_UNLOCK"] = "Unlock bar"
|
||||
L["OPT_EA_LURA_ACTIONBAR_AUTO_SHOW"] = "Auto show in boss room"
|
||||
L["OPT_EA_LURA_ACTIONBAR_ORIENTATION"] = "Orientation"
|
||||
L["OPT_EA_LURA_ACTIONBAR_HORIZONTAL"] = "Horizontal"
|
||||
L["OPT_EA_LURA_ACTIONBAR_VERTICAL"] = "Vertical"
|
||||
L["OPT_EA_LURA_ACTIONBAR_HINT"] = "Click rune buttons in the observed order. Slot 5 sends the sequence automatically. The red button clears the local sequence."
|
||||
L["OPT_EA_LURA_ICON_SIZE"] = "Icon size"
|
||||
L["OPT_EA_LURA_BACKGROUND_ALPHA"] = "Background alpha"
|
||||
L["OPT_EA_LURA_ICON_SPACING"] = "Icon spacing"
|
||||
L["OPT_EA_LURA_BORDER_ENABLED"] = "Show border"
|
||||
L["OPT_EA_LURA_BORDER_WIDTH"] = "Border width"
|
||||
L["OPT_EA_LURA_BORDER_COLOR"] = "Border color"
|
||||
L["OPT_EA_LURA_SHOW_LABELS"] = "Show labels"
|
||||
L["OPT_EA_LURA_RUNE_EMPTY"] = "Empty"
|
||||
L["OPT_EA_LURA_DRAG_HINT"] = "Drag to move"
|
||||
L["OPT_EA_LURA_BOSS"] = "Boss"
|
||||
L["OPT_EA_LURA_TANK"] = "Tank"
|
||||
L["DEVTOOLS_WINDOW_TITLE"] = "HMGT Developer Tools"
|
||||
L["DEVTOOLS_WINDOW_HINT"] = "Structured developer events for the current session"
|
||||
L["OPT_SYNC_REMOTE_CHARGES"] = "Sync remote charges"
|
||||
|
||||
102
Modules/EncounterAlerts/EncounterAlerts.lua
Normal file
102
Modules/EncounterAlerts/EncounterAlerts.lua
Normal file
@@ -0,0 +1,102 @@
|
||||
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 EA = HMGT:NewModule("EncounterAlerts", "AceEvent-3.0")
|
||||
HMGT.EncounterAlerts = EA
|
||||
|
||||
EA.runtimeEnabled = false
|
||||
|
||||
function EA:GetSettings()
|
||||
local profile = HMGT.db and HMGT.db.profile
|
||||
profile = profile or {}
|
||||
profile.encounterAlerts = type(profile.encounterAlerts) == "table" and profile.encounterAlerts or {}
|
||||
return profile.encounterAlerts
|
||||
end
|
||||
|
||||
function EA:GetLuraRunesSettings()
|
||||
local settings = self:GetSettings()
|
||||
settings.luraRunes = type(settings.luraRunes) == "table" and settings.luraRunes or {}
|
||||
return settings.luraRunes
|
||||
end
|
||||
|
||||
function EA:OnEnable()
|
||||
local settings = self:GetSettings()
|
||||
self.runtimeEnabled = settings.enabled == true
|
||||
self:RegisterEvent("PLAYER_ENTERING_WORLD", "RefreshLuraRunesContext")
|
||||
self:RegisterEvent("ZONE_CHANGED", "RefreshLuraRunesContext")
|
||||
self:RegisterEvent("ZONE_CHANGED_INDOORS", "RefreshLuraRunesContext")
|
||||
self:RegisterEvent("ZONE_CHANGED_NEW_AREA", "RefreshLuraRunesContext")
|
||||
self:RegisterEvent("GROUP_ROSTER_UPDATE", "RefreshLuraRunesContext")
|
||||
self:RegisterEvent("INSTANCE_ENCOUNTER_ENGAGE_UNIT", "RefreshLuraRunesContext")
|
||||
self:RegisterEvent("PLAYER_TARGET_CHANGED", "RefreshLuraRunesContext")
|
||||
self:RegisterEvent("ENCOUNTER_START", "HandleLuraEncounterStart")
|
||||
self:RegisterEvent("ENCOUNTER_END", "HandleLuraEncounterEnd")
|
||||
self:RegisterEvent("CHAT_MSG_RAID", "HandleLuraRaidChat")
|
||||
self:RegisterEvent("CHAT_MSG_RAID_LEADER", "HandleLuraRaidChat")
|
||||
if self.LuraRunes and self.LuraRunes.Refresh then
|
||||
self.LuraRunes:Refresh()
|
||||
end
|
||||
end
|
||||
|
||||
function EA:Enable()
|
||||
local settings = self:GetSettings()
|
||||
settings.enabled = true
|
||||
self.runtimeEnabled = true
|
||||
if self.LuraRunes and self.LuraRunes.Refresh then
|
||||
self.LuraRunes:Refresh()
|
||||
end
|
||||
end
|
||||
|
||||
function EA:Disable()
|
||||
local settings = self:GetSettings()
|
||||
settings.enabled = false
|
||||
self.runtimeEnabled = false
|
||||
if self.LuraRunes and self.LuraRunes.Hide then
|
||||
self.LuraRunes:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
function EA:GetDisplayName()
|
||||
return L["OPT_MODULE_ENCOUNTER_ALERTS"] or "Encounter Alerts"
|
||||
end
|
||||
|
||||
function EA:RefreshLuraRunesContext(event)
|
||||
if self.LuraRunes and self.LuraRunes.RefreshContext then
|
||||
self.LuraRunes:RefreshContext(event)
|
||||
end
|
||||
end
|
||||
|
||||
function EA:HandleLuraEncounterStart(_, encounterId, encounterName)
|
||||
if self.LuraRunes and self.LuraRunes.OnEncounterStart then
|
||||
self.LuraRunes:OnEncounterStart(encounterId, encounterName)
|
||||
end
|
||||
end
|
||||
|
||||
function EA:HandleLuraEncounterEnd(_, encounterId)
|
||||
if self.LuraRunes and self.LuraRunes.OnEncounterEnd then
|
||||
self.LuraRunes:OnEncounterEnd(encounterId)
|
||||
end
|
||||
end
|
||||
|
||||
function EA:HandleLuraRunesComm(senderName, payload)
|
||||
if self.LuraRunes and self.LuraRunes.HandleComm then
|
||||
self.LuraRunes:HandleComm(senderName, payload)
|
||||
end
|
||||
end
|
||||
|
||||
function EA:HandleLuraRaidChat(event, message, senderName)
|
||||
if self.LuraRunes and self.LuraRunes.HandleRaidChatMessage then
|
||||
self.LuraRunes:HandleRaidChatMessage(message, senderName, event)
|
||||
end
|
||||
end
|
||||
|
||||
function EA:HandleSlashCommand(input)
|
||||
if self.LuraRunes and self.LuraRunes.HandleSlashCommand then
|
||||
self.LuraRunes:HandleSlashCommand(input)
|
||||
else
|
||||
HMGT:OpenConfig()
|
||||
end
|
||||
end
|
||||
414
Modules/EncounterAlerts/EncounterAlertsOptions.lua
Normal file
414
Modules/EncounterAlerts/EncounterAlertsOptions.lua
Normal file
@@ -0,0 +1,414 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT 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 function NotifyOptionsChanged()
|
||||
if AceConfigRegistry and type(AceConfigRegistry.NotifyChange) == "function" then
|
||||
AceConfigRegistry:NotifyChange(ADDON_NAME)
|
||||
end
|
||||
end
|
||||
|
||||
local function GetSettings()
|
||||
local profile = HMGT.db and HMGT.db.profile
|
||||
if not profile then
|
||||
return {}
|
||||
end
|
||||
profile.encounterAlerts = type(profile.encounterAlerts) == "table" and profile.encounterAlerts or {}
|
||||
return profile.encounterAlerts
|
||||
end
|
||||
|
||||
local function GetLuraSettings()
|
||||
local settings = GetSettings()
|
||||
settings.luraRunes = type(settings.luraRunes) == "table" and settings.luraRunes or {}
|
||||
settings.luraRunes.slots = type(settings.luraRunes.slots) == "table" and settings.luraRunes.slots or {}
|
||||
settings.luraRunes.actionBar = type(settings.luraRunes.actionBar) == "table" and settings.luraRunes.actionBar or {}
|
||||
return settings.luraRunes
|
||||
end
|
||||
|
||||
local function GetLuraActionBarSettings()
|
||||
local settings = GetLuraSettings()
|
||||
settings.actionBar.shown = settings.actionBar.shown == true
|
||||
settings.actionBar.autoShow = settings.actionBar.autoShow ~= false
|
||||
settings.actionBar.unlocked = settings.actionBar.unlocked == true
|
||||
settings.actionBar.iconSize = tonumber(settings.actionBar.iconSize) or 42
|
||||
settings.actionBar.iconSpacing = tonumber(settings.actionBar.iconSpacing) or 8
|
||||
settings.actionBar.orientation = tostring(settings.actionBar.orientation or "horizontal")
|
||||
if settings.actionBar.orientation ~= "vertical" then
|
||||
settings.actionBar.orientation = "horizontal"
|
||||
end
|
||||
settings.actionBar.border = type(settings.actionBar.border) == "table" and settings.actionBar.border or {}
|
||||
return settings.actionBar
|
||||
end
|
||||
|
||||
local function GetLuraBorderSettings()
|
||||
local actionBar = GetLuraActionBarSettings()
|
||||
actionBar.border.enabled = actionBar.border.enabled == true
|
||||
actionBar.border.width = tonumber(actionBar.border.width) or 2
|
||||
actionBar.border.color = type(actionBar.border.color) == "table" and actionBar.border.color or {}
|
||||
actionBar.border.color.r = tonumber(actionBar.border.color.r) or 1
|
||||
actionBar.border.color.g = tonumber(actionBar.border.color.g) or 0.82
|
||||
actionBar.border.color.b = tonumber(actionBar.border.color.b) or 0.1
|
||||
actionBar.border.color.a = tonumber(actionBar.border.color.a) or 0.9
|
||||
return actionBar.border
|
||||
end
|
||||
|
||||
local function GetActionBarOrientationValues()
|
||||
return {
|
||||
horizontal = L["OPT_EA_LURA_ACTIONBAR_HORIZONTAL"] or "Horizontal",
|
||||
vertical = L["OPT_EA_LURA_ACTIONBAR_VERTICAL"] or "Vertical",
|
||||
}
|
||||
end
|
||||
|
||||
local function GetLuraRunes()
|
||||
return HMGT.EncounterAlerts and HMGT.EncounterAlerts.LuraRunes or nil
|
||||
end
|
||||
|
||||
local function RefreshEncounterAlerts()
|
||||
if HMGT.EncounterAlerts then
|
||||
local settings = GetSettings()
|
||||
HMGT.EncounterAlerts.runtimeEnabled = settings.enabled == true
|
||||
if HMGT.EncounterAlerts.LuraRunes and HMGT.EncounterAlerts.LuraRunes.Refresh then
|
||||
HMGT.EncounterAlerts.LuraRunes:Refresh()
|
||||
end
|
||||
end
|
||||
NotifyOptionsChanged()
|
||||
end
|
||||
|
||||
local function BuildRuneWindowOptions()
|
||||
return {
|
||||
type = "group",
|
||||
inline = true,
|
||||
order = 2,
|
||||
name = L["OPT_EA_LURA_RUNE_WINDOW"] or "Rune window",
|
||||
args = {
|
||||
unlocked = {
|
||||
type = "toggle",
|
||||
order = 1,
|
||||
width = "double",
|
||||
name = L["OPT_EA_LURA_UNLOCK"] or "Unlock rune frame",
|
||||
get = function()
|
||||
return GetLuraSettings().unlocked == true
|
||||
end,
|
||||
set = function(_, value)
|
||||
GetSettings().enabled = true
|
||||
local settings = GetLuraSettings()
|
||||
settings.enabled = true
|
||||
settings.unlocked = value == true
|
||||
RefreshEncounterAlerts()
|
||||
end,
|
||||
},
|
||||
hint = {
|
||||
type = "description",
|
||||
order = 2,
|
||||
width = "full",
|
||||
name = L["OPT_EA_LURA_HINT"] or "First version: normal/heroic layout only. Tank reference is placed bottom-center between slot 1 and 5.",
|
||||
},
|
||||
show = {
|
||||
type = "execute",
|
||||
order = 3,
|
||||
width = 0.8,
|
||||
name = L["OPT_EA_LURA_SHOW"] or "Show",
|
||||
func = function()
|
||||
local lura = GetLuraRunes()
|
||||
if lura and lura.Show then
|
||||
lura:Show()
|
||||
end
|
||||
end,
|
||||
},
|
||||
test = {
|
||||
type = "execute",
|
||||
order = 4,
|
||||
width = 0.9,
|
||||
name = L["OPT_EA_LURA_TEST"] or "Test pattern",
|
||||
func = function()
|
||||
local lura = GetLuraRunes()
|
||||
if lura and lura.Show and lura.ApplyTestPattern then
|
||||
lura:Show()
|
||||
lura:ApplyTestPattern()
|
||||
end
|
||||
end,
|
||||
},
|
||||
clear = {
|
||||
type = "execute",
|
||||
order = 5,
|
||||
width = 0.8,
|
||||
name = L["OPT_EA_LURA_CLEAR"] or "Clear",
|
||||
func = function()
|
||||
local lura = GetLuraRunes()
|
||||
if lura and lura.ClearAssignments then
|
||||
lura:ClearAssignments(false)
|
||||
end
|
||||
end,
|
||||
},
|
||||
broadcast = {
|
||||
type = "execute",
|
||||
order = 6,
|
||||
width = 1.2,
|
||||
name = L["OPT_EA_LURA_BROADCAST"] or "Broadcast",
|
||||
disabled = function()
|
||||
local lura = GetLuraRunes()
|
||||
return lura and lura.CanBroadcastSequence and not lura:CanBroadcastSequence() or false
|
||||
end,
|
||||
func = function()
|
||||
local lura = GetLuraRunes()
|
||||
if lura and lura.BroadcastAssignments then
|
||||
lura:BroadcastAssignments()
|
||||
end
|
||||
end,
|
||||
},
|
||||
iconSize = {
|
||||
type = "range",
|
||||
order = 7,
|
||||
width = 1.1,
|
||||
min = 28,
|
||||
max = 80,
|
||||
step = 1,
|
||||
name = L["OPT_EA_LURA_ICON_SIZE"] or "Icon size",
|
||||
get = function()
|
||||
return tonumber(GetLuraSettings().iconSize) or 44
|
||||
end,
|
||||
set = function(_, value)
|
||||
GetLuraSettings().iconSize = tonumber(value) or 44
|
||||
RefreshEncounterAlerts()
|
||||
end,
|
||||
},
|
||||
backgroundAlpha = {
|
||||
type = "range",
|
||||
order = 8,
|
||||
width = 1.1,
|
||||
min = 0,
|
||||
max = 0.8,
|
||||
step = 0.01,
|
||||
name = L["OPT_EA_LURA_BACKGROUND_ALPHA"] or "Background alpha",
|
||||
get = function()
|
||||
return tonumber(GetLuraSettings().backgroundAlpha) or 0.14
|
||||
end,
|
||||
set = function(_, value)
|
||||
GetLuraSettings().backgroundAlpha = tonumber(value) or 0.14
|
||||
RefreshEncounterAlerts()
|
||||
end,
|
||||
},
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
local function BuildRuneActionBarOptions()
|
||||
return {
|
||||
type = "group",
|
||||
inline = true,
|
||||
order = 3,
|
||||
name = L["OPT_EA_LURA_ACTIONBAR"] or "Rune action bar",
|
||||
args = {
|
||||
shown = {
|
||||
type = "toggle",
|
||||
order = 1,
|
||||
width = 1.1,
|
||||
name = L["OPT_EA_LURA_ACTIONBAR_SHOW"] or "Show bar",
|
||||
get = function()
|
||||
return GetLuraActionBarSettings().shown == true
|
||||
end,
|
||||
set = function(_, value)
|
||||
GetSettings().enabled = true
|
||||
local settings = GetLuraSettings()
|
||||
settings.enabled = true
|
||||
GetLuraActionBarSettings().shown = value == true
|
||||
RefreshEncounterAlerts()
|
||||
end,
|
||||
},
|
||||
unlocked = {
|
||||
type = "toggle",
|
||||
order = 2,
|
||||
width = 1.2,
|
||||
name = L["OPT_EA_LURA_ACTIONBAR_UNLOCK"] or "Unlock bar",
|
||||
get = function()
|
||||
return GetLuraActionBarSettings().unlocked == true
|
||||
end,
|
||||
set = function(_, value)
|
||||
GetSettings().enabled = true
|
||||
local settings = GetLuraSettings()
|
||||
settings.enabled = true
|
||||
local actionBar = GetLuraActionBarSettings()
|
||||
actionBar.shown = true
|
||||
actionBar.unlocked = value == true
|
||||
RefreshEncounterAlerts()
|
||||
end,
|
||||
},
|
||||
autoShow = {
|
||||
type = "toggle",
|
||||
order = 2.5,
|
||||
width = 1.4,
|
||||
name = L["OPT_EA_LURA_ACTIONBAR_AUTO_SHOW"] or "Auto show in boss room",
|
||||
get = function()
|
||||
return GetLuraActionBarSettings().autoShow == true
|
||||
end,
|
||||
set = function(_, value)
|
||||
GetLuraActionBarSettings().autoShow = value == true
|
||||
RefreshEncounterAlerts()
|
||||
end,
|
||||
},
|
||||
orientation = {
|
||||
type = "select",
|
||||
order = 3,
|
||||
width = 1.2,
|
||||
name = L["OPT_EA_LURA_ACTIONBAR_ORIENTATION"] or "Orientation",
|
||||
values = GetActionBarOrientationValues,
|
||||
get = function()
|
||||
return GetLuraActionBarSettings().orientation
|
||||
end,
|
||||
set = function(_, value)
|
||||
local actionBar = GetLuraActionBarSettings()
|
||||
actionBar.orientation = tostring(value or "horizontal")
|
||||
RefreshEncounterAlerts()
|
||||
end,
|
||||
},
|
||||
iconSize = {
|
||||
type = "range",
|
||||
order = 5,
|
||||
width = 1.1,
|
||||
min = 28,
|
||||
max = 80,
|
||||
step = 1,
|
||||
name = L["OPT_EA_LURA_ICON_SIZE"] or "Icon size",
|
||||
get = function()
|
||||
return tonumber(GetLuraActionBarSettings().iconSize) or 42
|
||||
end,
|
||||
set = function(_, value)
|
||||
GetLuraActionBarSettings().iconSize = tonumber(value) or 42
|
||||
RefreshEncounterAlerts()
|
||||
end,
|
||||
},
|
||||
iconSpacing = {
|
||||
type = "range",
|
||||
order = 6,
|
||||
width = 1.1,
|
||||
min = 0,
|
||||
max = 80,
|
||||
step = 1,
|
||||
name = L["OPT_EA_LURA_ICON_SPACING"] or "Icon spacing",
|
||||
get = function()
|
||||
return tonumber(GetLuraActionBarSettings().iconSpacing) or 8
|
||||
end,
|
||||
set = function(_, value)
|
||||
GetLuraActionBarSettings().iconSpacing = tonumber(value) or 8
|
||||
RefreshEncounterAlerts()
|
||||
end,
|
||||
},
|
||||
borderEnabled = {
|
||||
type = "toggle",
|
||||
order = 7,
|
||||
width = 1.1,
|
||||
name = L["OPT_EA_LURA_BORDER_ENABLED"] or "Show border",
|
||||
get = function()
|
||||
return GetLuraBorderSettings().enabled == true
|
||||
end,
|
||||
set = function(_, value)
|
||||
GetLuraBorderSettings().enabled = value == true
|
||||
RefreshEncounterAlerts()
|
||||
end,
|
||||
},
|
||||
borderWidth = {
|
||||
type = "range",
|
||||
order = 8,
|
||||
width = 1.1,
|
||||
min = 1,
|
||||
max = 12,
|
||||
step = 1,
|
||||
name = L["OPT_EA_LURA_BORDER_WIDTH"] or "Border width",
|
||||
disabled = function()
|
||||
return GetLuraBorderSettings().enabled ~= true
|
||||
end,
|
||||
get = function()
|
||||
return tonumber(GetLuraBorderSettings().width) or 2
|
||||
end,
|
||||
set = function(_, value)
|
||||
GetLuraBorderSettings().width = tonumber(value) or 2
|
||||
RefreshEncounterAlerts()
|
||||
end,
|
||||
},
|
||||
borderColor = {
|
||||
type = "color",
|
||||
order = 9,
|
||||
width = 1.1,
|
||||
hasAlpha = true,
|
||||
name = L["OPT_EA_LURA_BORDER_COLOR"] or "Border color",
|
||||
disabled = function()
|
||||
return GetLuraBorderSettings().enabled ~= true
|
||||
end,
|
||||
get = function()
|
||||
local color = GetLuraBorderSettings().color
|
||||
return color.r or 1, color.g or 0.82, color.b or 0.1, color.a or 0.9
|
||||
end,
|
||||
set = function(_, r, g, b, a)
|
||||
local color = GetLuraBorderSettings().color
|
||||
color.r = tonumber(r) or 1
|
||||
color.g = tonumber(g) or 0.82
|
||||
color.b = tonumber(b) or 0.1
|
||||
color.a = tonumber(a) or 0.9
|
||||
RefreshEncounterAlerts()
|
||||
end,
|
||||
},
|
||||
hint = {
|
||||
type = "description",
|
||||
order = 10,
|
||||
width = "full",
|
||||
name = L["OPT_EA_LURA_ACTIONBAR_HINT"] or "Click rune buttons in the observed order. Slot 5 sends the sequence automatically.",
|
||||
},
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
HMGT_Config:RegisterOptionsProvider("encounterAlerts", function()
|
||||
return {
|
||||
path = "encounterAlerts",
|
||||
order = 50,
|
||||
group = {
|
||||
type = "group",
|
||||
name = L["OPT_MODULE_ENCOUNTER_ALERTS"] or "Encounter Alerts",
|
||||
order = 50,
|
||||
childGroups = "tab",
|
||||
args = {
|
||||
general = {
|
||||
type = "group",
|
||||
name = L["OPT_GENERAL"] or "General",
|
||||
order = 1,
|
||||
args = {
|
||||
description = {
|
||||
type = "description",
|
||||
order = 1,
|
||||
width = "full",
|
||||
name = L["OPT_ENCOUNTER_ALERTS_PLACEHOLDER"] or "Encounter-specific helper frames and alerts.",
|
||||
},
|
||||
},
|
||||
},
|
||||
luraRunes = {
|
||||
type = "group",
|
||||
name = L["OPT_EA_LURA_TITLE"] or "L'ura Runes",
|
||||
order = 2,
|
||||
args = {
|
||||
enabled = {
|
||||
type = "toggle",
|
||||
order = 1,
|
||||
width = "double",
|
||||
name = L["OPT_EA_LURA_ENABLED"] or "Enable L'ura runes",
|
||||
get = function()
|
||||
return GetLuraSettings().enabled == true
|
||||
end,
|
||||
set = function(_, value)
|
||||
local enabled = value == true
|
||||
GetSettings().enabled = enabled
|
||||
GetLuraSettings().enabled = enabled
|
||||
RefreshEncounterAlerts()
|
||||
end,
|
||||
},
|
||||
runeWindow = BuildRuneWindowOptions(),
|
||||
actionBar = BuildRuneActionBarOptions(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
end)
|
||||
1113
Modules/EncounterAlerts/LuraRunes.lua
Normal file
1113
Modules/EncounterAlerts/LuraRunes.lua
Normal file
File diff suppressed because it is too large
Load Diff
1
Modules/EncounterAlerts/Media/LuraRunes/.gitkeep
Normal file
1
Modules/EncounterAlerts/Media/LuraRunes/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
BIN
Modules/EncounterAlerts/Media/LuraRunes/Rune_Circle.tga
Normal file
BIN
Modules/EncounterAlerts/Media/LuraRunes/Rune_Circle.tga
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
BIN
Modules/EncounterAlerts/Media/LuraRunes/Rune_Diamond.tga
Normal file
BIN
Modules/EncounterAlerts/Media/LuraRunes/Rune_Diamond.tga
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
BIN
Modules/EncounterAlerts/Media/LuraRunes/Rune_T.tga
Normal file
BIN
Modules/EncounterAlerts/Media/LuraRunes/Rune_T.tga
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
BIN
Modules/EncounterAlerts/Media/LuraRunes/Rune_Triangle.tga
Normal file
BIN
Modules/EncounterAlerts/Media/LuraRunes/Rune_Triangle.tga
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
BIN
Modules/EncounterAlerts/Media/LuraRunes/Rune_X.tga
Normal file
BIN
Modules/EncounterAlerts/Media/LuraRunes/Rune_X.tga
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
@@ -1,8 +0,0 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
local RT = HMGT.RaidTimeline
|
||||
if not RT then return end
|
||||
|
||||
-- Placeholder for later DBM-specific raid timeline integration.
|
||||
@@ -1,692 +1,69 @@
|
||||
-- Modules/GroupCooldownTracker.lua
|
||||
-- Group-Cooldown-Tracker Modul (ein Frame pro Spieler in der Gruppe)
|
||||
|
||||
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)
|
||||
|
||||
local GCT = HMGT:NewModule("GroupCooldownTracker")
|
||||
HMGT.GroupCooldownTracker = GCT
|
||||
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
|
||||
|
||||
GCT.frame = nil
|
||||
GCT.frames = {}
|
||||
local module = HMGT:NewModule("GroupCooldownTracker")
|
||||
HMGT.GroupCooldownTracker = module
|
||||
|
||||
local function SanitizeFrameToken(name)
|
||||
if not name or name == "" then return "Unknown" end
|
||||
return name:gsub("[^%w_]", "_")
|
||||
end
|
||||
|
||||
local function ShortName(name)
|
||||
if not name then return "" end
|
||||
local short = name:match("^[^-]+")
|
||||
return short or name
|
||||
end
|
||||
|
||||
local function IsUsableAnchorFrame(frame)
|
||||
return frame
|
||||
and frame.IsObjectType
|
||||
and (frame:IsObjectType("Frame") or frame:IsObjectType("Button"))
|
||||
end
|
||||
|
||||
local function GetFrameUnit(frame)
|
||||
if not frame then return nil end
|
||||
local unit = frame.unit
|
||||
if not unit and frame.GetAttribute then
|
||||
unit = frame:GetAttribute("unit")
|
||||
end
|
||||
return unit
|
||||
end
|
||||
|
||||
local function FrameMatchesUnit(frame, unitId)
|
||||
if not IsUsableAnchorFrame(frame) then return false end
|
||||
if not unitId then return true end
|
||||
local unit = GetFrameUnit(frame)
|
||||
return unit == unitId
|
||||
end
|
||||
|
||||
local PLAYER_FRAME_CANDIDATES = {
|
||||
"PlayerFrame",
|
||||
"ElvUF_Player",
|
||||
"NephUI_PlayerFrame",
|
||||
"NephUIPlayerFrame",
|
||||
"oUF_NephUI_Player",
|
||||
"SUFUnitplayer",
|
||||
module.definition = {
|
||||
moduleName = "GroupCooldownTracker",
|
||||
dbKey = "groupCooldownTracker",
|
||||
trackerType = "group",
|
||||
trackerKey = "groupCooldownTracker",
|
||||
title = function()
|
||||
return L["GCD_TITLE"]
|
||||
end,
|
||||
categories = { "tank", "defensive", "healing", "cc", "utility", "offensive", "lust", "interrupt" },
|
||||
}
|
||||
|
||||
local PARTY_FRAME_PATTERNS = {
|
||||
"PartyMemberFrame%d", -- Blizzard alt
|
||||
"CompactPartyFrameMember%d", -- Blizzard modern
|
||||
"ElvUF_PartyGroup1UnitButton%d", -- ElvUI
|
||||
"ElvUF_PartyUnitButton%d", -- ElvUI variant
|
||||
"NephUI_PartyUnitButton%d", -- NephUI (common naming variants)
|
||||
"NephUI_PartyFrame%d",
|
||||
"NephUIPartyFrame%d",
|
||||
"oUF_NephUI_PartyUnitButton%d",
|
||||
"SUFUnitparty%d", -- Shadowed Unit Frames
|
||||
}
|
||||
|
||||
local unitFrameCache = {}
|
||||
|
||||
local function EntryNeedsVisualTicker(entry)
|
||||
if type(entry) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
local remaining = tonumber(entry.remaining) or 0
|
||||
if remaining > 0 then
|
||||
return true
|
||||
end
|
||||
|
||||
local maxCharges = tonumber(entry.maxCharges) or 0
|
||||
local currentCharges = tonumber(entry.currentCharges)
|
||||
if maxCharges > 0 and currentCharges ~= nil and currentCharges < maxCharges then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
function module:GetDefinition()
|
||||
return self.definition
|
||||
end
|
||||
|
||||
local function BuildAnchorLayoutSignature(settings, ordered, unitByPlayer)
|
||||
local parts = {
|
||||
settings.attachToPartyFrame == true and "attach" or "stack",
|
||||
tostring(settings.partyAttachSide or "RIGHT"),
|
||||
tostring(tonumber(settings.partyAttachOffsetX) or 8),
|
||||
tostring(tonumber(settings.partyAttachOffsetY) or 0),
|
||||
tostring(settings.showBar and "bar" or "icon"),
|
||||
tostring(settings.growDirection or "DOWN"),
|
||||
tostring(settings.width or 250),
|
||||
tostring(settings.barHeight or 20),
|
||||
tostring(settings.iconSize or 32),
|
||||
tostring(settings.iconCols or 6),
|
||||
tostring(settings.barSpacing or 2),
|
||||
tostring(settings.locked),
|
||||
tostring(settings.anchorTo or "UIParent"),
|
||||
tostring(settings.anchorPoint or "TOPLEFT"),
|
||||
tostring(settings.anchorRelPoint or "TOPLEFT"),
|
||||
tostring(settings.anchorX or settings.posX or 0),
|
||||
tostring(settings.anchorY or settings.posY or 0),
|
||||
}
|
||||
|
||||
for _, playerName in ipairs(ordered or {}) do
|
||||
parts[#parts + 1] = tostring(playerName)
|
||||
parts[#parts + 1] = tostring(unitByPlayer and unitByPlayer[playerName] or "")
|
||||
end
|
||||
|
||||
return table.concat(parts, "|")
|
||||
end
|
||||
|
||||
local function ResolveNamedUnitFrame(unitId)
|
||||
if unitId == "player" then
|
||||
for _, frameName in ipairs(PLAYER_FRAME_CANDIDATES) do
|
||||
local frame = _G[frameName]
|
||||
if FrameMatchesUnit(frame, unitId) or (frameName == "PlayerFrame" and IsUsableAnchorFrame(frame)) then
|
||||
return frame
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local idx = type(unitId) == "string" and unitId:match("^party(%d+)$")
|
||||
if not idx then
|
||||
return nil
|
||||
end
|
||||
|
||||
idx = tonumber(idx)
|
||||
for _, pattern in ipairs(PARTY_FRAME_PATTERNS) do
|
||||
local frame = _G[pattern:format(idx)]
|
||||
if FrameMatchesUnit(frame, unitId) then
|
||||
return frame
|
||||
function module:GetSettings()
|
||||
for _, tracker in ipairs(HMGT:GetTrackerConfigs()) do
|
||||
if tracker.trackerKey == self.definition.trackerKey then
|
||||
return tracker
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function ScanUnitFrame(unitId)
|
||||
local frame = EnumerateFrames()
|
||||
local scanned = 0
|
||||
while frame and scanned < 8000 do
|
||||
if IsUsableAnchorFrame(frame) then
|
||||
local unit = GetFrameUnit(frame)
|
||||
if unit == unitId then
|
||||
HMGT:DebugScoped("verbose", HMGT:GetTrackerDebugScope("Group Cooldowns"), "GroupAttach scan unit=%s scanned=%d found=true", tostring(unitId), scanned)
|
||||
return frame
|
||||
end
|
||||
end
|
||||
scanned = scanned + 1
|
||||
frame = EnumerateFrames(frame)
|
||||
end
|
||||
HMGT:DebugScoped("verbose", HMGT:GetTrackerDebugScope("Group Cooldowns"), "GroupAttach scan unit=%s scanned=%d found=false", tostring(unitId), scanned)
|
||||
return nil
|
||||
end
|
||||
|
||||
local function ResolveUnitAnchorFrame(unitId)
|
||||
if not unitId then return nil end
|
||||
|
||||
local now = GetTime()
|
||||
local cached = unitFrameCache[unitId]
|
||||
if cached and now < (cached.expires or 0) then
|
||||
if cached.frame and cached.frame:IsShown() then
|
||||
return cached.frame
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local frame = ResolveNamedUnitFrame(unitId)
|
||||
if not frame then
|
||||
frame = ScanUnitFrame(unitId)
|
||||
end
|
||||
|
||||
local expiresIn = 1.0
|
||||
if frame and frame:IsShown() then
|
||||
expiresIn = 10.0
|
||||
end
|
||||
unitFrameCache[unitId] = {
|
||||
frame = frame,
|
||||
expires = now + expiresIn,
|
||||
}
|
||||
|
||||
if frame and frame:IsShown() then
|
||||
return frame
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function GCT:GetFrameIdForPlayer(playerName)
|
||||
return "GroupCooldownTracker_" .. SanitizeFrameToken(playerName)
|
||||
end
|
||||
|
||||
function GCT:GetPlayerFrame(playerName)
|
||||
if not playerName then return nil end
|
||||
return self.frames[playerName]
|
||||
end
|
||||
|
||||
function GCT:GetAnchorableFrames()
|
||||
return self.frames
|
||||
end
|
||||
|
||||
function GCT:EnsurePlayerFrame(playerName)
|
||||
local frame = self.frames[playerName]
|
||||
local s = HMGT.db.profile.groupCooldownTracker
|
||||
if frame then
|
||||
return frame
|
||||
end
|
||||
|
||||
frame = HMGT.TrackerFrame:CreateTrackerFrame(self:GetFrameIdForPlayer(playerName), s)
|
||||
frame._hmgtPlayerName = playerName
|
||||
self.frames[playerName] = frame
|
||||
return frame
|
||||
end
|
||||
|
||||
function GCT:HideAllFrames()
|
||||
for _, frame in pairs(self.frames) do
|
||||
frame:Hide()
|
||||
end
|
||||
self.activeOrder = nil
|
||||
self.unitByPlayer = nil
|
||||
self.frame = nil
|
||||
self._lastAnchorLayoutSignature = nil
|
||||
self._nextAnchorRetryAt = nil
|
||||
end
|
||||
|
||||
function GCT:SetLockedAll(locked)
|
||||
for _, frame in pairs(self.frames) do
|
||||
HMGT.TrackerFrame:SetLocked(frame, locked)
|
||||
function module:Enable()
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.Enable then
|
||||
HMGT.TrackerManager:Enable()
|
||||
end
|
||||
end
|
||||
|
||||
function GCT:EnsureUpdateTicker()
|
||||
if self.updateTicker then
|
||||
return
|
||||
end
|
||||
self.updateTicker = C_Timer.NewTicker(0.1, function()
|
||||
self:UpdateDisplay()
|
||||
end)
|
||||
end
|
||||
|
||||
function GCT:StopUpdateTicker()
|
||||
if self.updateTicker then
|
||||
self.updateTicker:Cancel()
|
||||
self.updateTicker = nil
|
||||
function module:Disable()
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.UpdateDisplay then
|
||||
HMGT.TrackerManager:UpdateDisplay()
|
||||
end
|
||||
end
|
||||
|
||||
function GCT:SetUpdateTickerEnabled(enabled)
|
||||
if enabled then
|
||||
self:EnsureUpdateTicker()
|
||||
else
|
||||
self:StopUpdateTicker()
|
||||
function module:SetLockedAll(locked)
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.SetAllLocked then
|
||||
HMGT.TrackerManager:SetAllLocked(locked)
|
||||
end
|
||||
end
|
||||
|
||||
function GCT:InvalidateAnchorLayout()
|
||||
self._lastAnchorLayoutSignature = nil
|
||||
self._nextAnchorRetryAt = nil
|
||||
function module:RefreshAnchors(force)
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.RefreshAnchors then
|
||||
HMGT.TrackerManager:RefreshAnchors(force)
|
||||
end
|
||||
end
|
||||
|
||||
function GCT:RefreshAnchors(force)
|
||||
local s = HMGT.db.profile.groupCooldownTracker
|
||||
if not s then return end
|
||||
|
||||
local ordered = {}
|
||||
for _, playerName in ipairs(self.activeOrder or {}) do
|
||||
local frame = self.frames[playerName]
|
||||
if frame and frame:IsShown() then
|
||||
table.insert(ordered, playerName)
|
||||
end
|
||||
function module:InvalidateAnchorLayout()
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.InvalidateAnchorLayout then
|
||||
HMGT.TrackerManager:InvalidateAnchorLayout()
|
||||
end
|
||||
|
||||
if #ordered == 0 then
|
||||
self.frame = nil
|
||||
self._lastAnchorLayoutSignature = nil
|
||||
self._nextAnchorRetryAt = nil
|
||||
return
|
||||
end
|
||||
|
||||
local now = GetTime()
|
||||
local signature = BuildAnchorLayoutSignature(s, ordered, self.unitByPlayer)
|
||||
if not force and self._lastAnchorLayoutSignature == signature then
|
||||
local retryAt = tonumber(self._nextAnchorRetryAt) or 0
|
||||
if retryAt <= 0 or now < retryAt then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- Do not force anchor updates while user is dragging a tracker frame.
|
||||
for _, playerName in ipairs(ordered) do
|
||||
local frame = self.frames[playerName]
|
||||
if frame and frame._hmgtDragging then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local primaryName = ordered[1]
|
||||
local primary = self.frames[primaryName]
|
||||
self.frame = primary
|
||||
|
||||
if s.attachToPartyFrame == true then
|
||||
local side = s.partyAttachSide or "RIGHT"
|
||||
local extraX = tonumber(s.partyAttachOffsetX) or 8
|
||||
local extraY = tonumber(s.partyAttachOffsetY) or 0
|
||||
local growsUp = s.showBar == true and s.growDirection == "UP"
|
||||
local barHeight = tonumber(s.barHeight) or 20
|
||||
local growUpAttachOffset = barHeight + 20
|
||||
local prevPlaced = nil
|
||||
local missingTargets = 0
|
||||
|
||||
for i = 1, #ordered do
|
||||
local playerName = ordered[i]
|
||||
local frame = self.frames[playerName]
|
||||
local unitId = self.unitByPlayer and self.unitByPlayer[playerName]
|
||||
local target = ResolveUnitAnchorFrame(unitId)
|
||||
local contentTopInset = HMGT.TrackerFrame.GetContentTopInset and HMGT.TrackerFrame:GetContentTopInset(frame) or 0
|
||||
|
||||
frame:ClearAllPoints()
|
||||
if target then
|
||||
if side == "LEFT" then
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMRIGHT", target, "TOPLEFT", -extraX, extraY - growUpAttachOffset)
|
||||
else
|
||||
frame:SetPoint("TOPRIGHT", target, "TOPLEFT", -extraX, extraY + contentTopInset)
|
||||
end
|
||||
else
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMLEFT", target, "TOPRIGHT", extraX, extraY - growUpAttachOffset)
|
||||
else
|
||||
frame:SetPoint("TOPLEFT", target, "TOPRIGHT", extraX, extraY + contentTopInset)
|
||||
end
|
||||
end
|
||||
elseif prevPlaced then
|
||||
missingTargets = missingTargets + 1
|
||||
HMGT:DebugScoped("verbose", HMGT:GetTrackerDebugScope("Group Cooldowns"), "GroupAttach fallback-stack player=%s unit=%s", tostring(playerName), tostring(unitId))
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMLEFT", prevPlaced, "TOPLEFT", 0, (s.barSpacing or 2) + 10)
|
||||
else
|
||||
frame:SetPoint("TOPLEFT", prevPlaced, "BOTTOMLEFT", 0, -((s.barSpacing or 2) + 10))
|
||||
end
|
||||
else
|
||||
missingTargets = missingTargets + 1
|
||||
HMGT:DebugScoped("info", HMGT:GetTrackerDebugScope("Group Cooldowns"), "GroupAttach fallback-anchor player=%s unit=%s (no party frame found)", tostring(playerName), tostring(unitId))
|
||||
HMGT.TrackerFrame:ApplyAnchor(frame)
|
||||
end
|
||||
|
||||
frame:EnableMouse(false)
|
||||
prevPlaced = frame
|
||||
end
|
||||
if missingTargets > 0 then
|
||||
self._lastAnchorLayoutSignature = nil
|
||||
self._nextAnchorRetryAt = now + 1.0
|
||||
else
|
||||
self._lastAnchorLayoutSignature = signature
|
||||
self._nextAnchorRetryAt = nil
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
HMGT.TrackerFrame:ApplyAnchor(primary)
|
||||
primary:EnableMouse(not s.locked)
|
||||
|
||||
local gap = (s.barSpacing or 2) + 10
|
||||
local growsUp = s.showBar == true and s.growDirection == "UP"
|
||||
for i = 2, #ordered do
|
||||
local prev = self.frames[ordered[i - 1]]
|
||||
local frame = self.frames[ordered[i]]
|
||||
frame:ClearAllPoints()
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMLEFT", prev, "TOPLEFT", 0, gap)
|
||||
else
|
||||
frame:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -gap)
|
||||
end
|
||||
frame:EnableMouse(false)
|
||||
end
|
||||
self._lastAnchorLayoutSignature = signature
|
||||
self._nextAnchorRetryAt = nil
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- ENABLE / DISABLE
|
||||
-- ============================================================
|
||||
|
||||
function GCT:Enable()
|
||||
local s = HMGT.db.profile.groupCooldownTracker
|
||||
if not s.enabled and not s.demoMode and not s.testMode then return end
|
||||
|
||||
self:UpdateDisplay()
|
||||
function module:GetAnchorableFrames()
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.GetAnchorableFrames then
|
||||
return HMGT.TrackerManager:GetAnchorableFrames()
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
function GCT:Disable()
|
||||
self:StopUpdateTicker()
|
||||
self:HideAllFrames()
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- DISPLAY UPDATE
|
||||
-- ============================================================
|
||||
|
||||
function GCT:UpdateDisplay()
|
||||
local s = HMGT.db.profile.groupCooldownTracker
|
||||
if not s then return end
|
||||
|
||||
if s.testMode then
|
||||
local entries, playerName = HMGT:GetOwnTestEntries(HMGT_SpellData.GroupCooldowns, s, {
|
||||
deferChargeCooldownUntilEmpty = false,
|
||||
})
|
||||
local byPlayer = { [playerName] = {} }
|
||||
for _, entry in ipairs(entries) do
|
||||
entry.playerName = playerName
|
||||
table.insert(byPlayer[playerName], entry)
|
||||
end
|
||||
|
||||
self.activeOrder = { playerName }
|
||||
self.unitByPlayer = { [playerName] = "player" }
|
||||
self.lastEntryCount = 0
|
||||
local active = {}
|
||||
local shownOrder = {}
|
||||
local shouldTick = false
|
||||
for _, pName in ipairs(self.activeOrder) do
|
||||
local frame = self:EnsurePlayerFrame(pName)
|
||||
HMGT.TrackerFrame:SetLocked(frame, s.locked)
|
||||
HMGT.TrackerFrame:SetTitle(frame, string.format("%s - %s", L["GCD_TITLE"], ShortName(pName)))
|
||||
local displayEntries = byPlayer[pName]
|
||||
if HMGT.FilterDisplayEntries then
|
||||
displayEntries = HMGT:FilterDisplayEntries(s, displayEntries) or displayEntries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(displayEntries, "groupCooldownTracker")
|
||||
end
|
||||
if #displayEntries > 0 then
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, displayEntries, true)
|
||||
self.lastEntryCount = self.lastEntryCount + #displayEntries
|
||||
frame:Show()
|
||||
active[pName] = true
|
||||
shownOrder[#shownOrder + 1] = pName
|
||||
for _, entry in ipairs(displayEntries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
self.activeOrder = shownOrder
|
||||
|
||||
for pn, frame in pairs(self.frames) do
|
||||
if not active[pn] then
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
self:RefreshAnchors()
|
||||
self:SetUpdateTickerEnabled(shouldTick)
|
||||
return
|
||||
end
|
||||
|
||||
if s.demoMode then
|
||||
local entries = HMGT:GetDemoEntries("groupCooldownTracker", HMGT_SpellData.GroupCooldowns, s)
|
||||
local playerName = HMGT:NormalizePlayerName(UnitName("player")) or "DemoPlayer"
|
||||
local byPlayer = { [playerName] = {} }
|
||||
for _, entry in ipairs(entries) do
|
||||
entry.playerName = playerName
|
||||
table.insert(byPlayer[playerName], entry)
|
||||
end
|
||||
|
||||
self.activeOrder = { playerName }
|
||||
self.unitByPlayer = { [playerName] = "player" }
|
||||
self.lastEntryCount = 0
|
||||
local active = {}
|
||||
local shownOrder = {}
|
||||
local shouldTick = false
|
||||
for _, playerName in ipairs(self.activeOrder) do
|
||||
local frame = self:EnsurePlayerFrame(playerName)
|
||||
HMGT.TrackerFrame:SetLocked(frame, s.locked)
|
||||
HMGT.TrackerFrame:SetTitle(frame, string.format("%s - %s", L["GCD_TITLE"], ShortName(playerName)))
|
||||
local displayEntries = byPlayer[playerName]
|
||||
if HMGT.FilterDisplayEntries then
|
||||
displayEntries = HMGT:FilterDisplayEntries(s, displayEntries) or displayEntries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(displayEntries, "groupCooldownTracker")
|
||||
end
|
||||
if #displayEntries > 0 then
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, displayEntries, true)
|
||||
self.lastEntryCount = self.lastEntryCount + #displayEntries
|
||||
frame:Show()
|
||||
active[playerName] = true
|
||||
shownOrder[#shownOrder + 1] = playerName
|
||||
shouldTick = true
|
||||
else
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
self.activeOrder = shownOrder
|
||||
|
||||
for pn, frame in pairs(self.frames) do
|
||||
if not active[pn] then
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
self:RefreshAnchors()
|
||||
self:SetUpdateTickerEnabled(shouldTick)
|
||||
return
|
||||
end
|
||||
|
||||
if IsInRaid() or not IsInGroup() then
|
||||
self.lastEntryCount = 0
|
||||
self:StopUpdateTicker()
|
||||
self:HideAllFrames()
|
||||
return
|
||||
end
|
||||
if not s.enabled then
|
||||
self.lastEntryCount = 0
|
||||
self:StopUpdateTicker()
|
||||
self:HideAllFrames()
|
||||
return
|
||||
end
|
||||
if not HMGT:IsVisibleForCurrentGroup(s) then
|
||||
self.lastEntryCount = 0
|
||||
self:StopUpdateTicker()
|
||||
self:HideAllFrames()
|
||||
return
|
||||
end
|
||||
|
||||
local entriesByPlayer, order, unitByPlayer = self:CollectEntriesByPlayer()
|
||||
self.activeOrder = order
|
||||
self.unitByPlayer = unitByPlayer
|
||||
self.lastEntryCount = 0
|
||||
local active = {}
|
||||
local shownOrder = {}
|
||||
local shouldTick = false
|
||||
|
||||
for _, playerName in ipairs(order) do
|
||||
local frame = self:EnsurePlayerFrame(playerName)
|
||||
HMGT.TrackerFrame:SetLocked(frame, s.locked)
|
||||
HMGT.TrackerFrame:SetTitle(frame, string.format("%s - %s", L["GCD_TITLE"], ShortName(playerName)))
|
||||
|
||||
local entries = entriesByPlayer[playerName] or {}
|
||||
if HMGT.FilterDisplayEntries then
|
||||
entries = HMGT:FilterDisplayEntries(s, entries) or entries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries, "groupCooldownTracker")
|
||||
end
|
||||
if #entries > 0 then
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
||||
self.lastEntryCount = self.lastEntryCount + #entries
|
||||
frame:Show()
|
||||
active[playerName] = true
|
||||
shownOrder[#shownOrder + 1] = playerName
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
self.activeOrder = shownOrder
|
||||
|
||||
for pn, frame in pairs(self.frames) do
|
||||
if not active[pn] then
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
self:RefreshAnchors()
|
||||
self:SetUpdateTickerEnabled(shouldTick)
|
||||
end
|
||||
|
||||
function GCT:CollectEntriesByPlayer()
|
||||
local s = HMGT.db.profile.groupCooldownTracker
|
||||
local byPlayer = {}
|
||||
local playerOrder = {}
|
||||
local unitByPlayer = {}
|
||||
local players = self:GetGroupPlayers()
|
||||
|
||||
for _, playerInfo in ipairs(players) do
|
||||
repeat
|
||||
local name = playerInfo.name
|
||||
if not name then break end
|
||||
|
||||
local pData = HMGT.playerData[name]
|
||||
local class = pData and pData.class or playerInfo.class
|
||||
local specIdx
|
||||
if playerInfo.isOwn then
|
||||
specIdx = GetSpecialization()
|
||||
if not specIdx or specIdx == 0 then break end
|
||||
else
|
||||
specIdx = pData and pData.specIndex or nil
|
||||
if not specIdx or tonumber(specIdx) <= 0 then break end
|
||||
end
|
||||
local talents = pData and pData.talents or {}
|
||||
if not class then break end
|
||||
|
||||
local knownCDs = HMGT_SpellData.GetSpellsForSpec(class, specIdx, HMGT_SpellData.GroupCooldowns)
|
||||
local entries = {}
|
||||
for _, spellEntry in ipairs(knownCDs) do
|
||||
if s.enabledSpells[spellEntry.spellId] ~= false then
|
||||
local remaining, total, curCharges, maxCharges = HMGT:GetCooldownInfo(name, spellEntry.spellId, {
|
||||
deferChargeCooldownUntilEmpty = false,
|
||||
})
|
||||
local isAvailabilitySpell = HMGT.IsAvailabilitySpell and HMGT:IsAvailabilitySpell(spellEntry)
|
||||
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
local hasChargeSpell = (tonumber(maxCharges) or 0) > 1
|
||||
local hasPartialCharges = (tonumber(maxCharges) or 0) > 0 and (tonumber(curCharges) or tonumber(maxCharges) or 0) < (tonumber(maxCharges) or 0)
|
||||
local include = HMGT:ShouldDisplayEntry(s, remaining, curCharges, maxCharges, spellEntry)
|
||||
local spellKnown = HMGT:IsTrackedSpellKnownForPlayer(name, spellEntry.spellId)
|
||||
local hasActiveCd = ((remaining or 0) > 0) or hasPartialCharges
|
||||
if not spellKnown and not hasActiveCd then
|
||||
include = false
|
||||
end
|
||||
if isAvailabilitySpell and not spellKnown then
|
||||
include = false
|
||||
end
|
||||
if not playerInfo.isOwn then
|
||||
if isAvailabilitySpell and not HMGT:HasAvailabilityState(name, spellEntry.spellId) then
|
||||
include = false
|
||||
end
|
||||
end
|
||||
if include then
|
||||
table.insert(entries, {
|
||||
playerName = name,
|
||||
class = class,
|
||||
spellEntry = spellEntry,
|
||||
remaining = remaining,
|
||||
total = total > 0 and total or effectiveCd,
|
||||
currentCharges = curCharges,
|
||||
maxCharges = maxCharges,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if #entries > 0 then
|
||||
byPlayer[name] = entries
|
||||
table.insert(playerOrder, name)
|
||||
unitByPlayer[name] = playerInfo.unitId
|
||||
end
|
||||
until true
|
||||
end
|
||||
|
||||
table.sort(playerOrder, function(a, b)
|
||||
local own = HMGT:NormalizePlayerName(UnitName("player"))
|
||||
if a == own and b ~= own then return true end
|
||||
if b == own and a ~= own then return false end
|
||||
return a < b
|
||||
end)
|
||||
|
||||
return byPlayer, playerOrder, unitByPlayer
|
||||
end
|
||||
|
||||
function GCT:GetGroupPlayers()
|
||||
local players = {}
|
||||
local ownName = HMGT:NormalizePlayerName(UnitName("player"))
|
||||
local settings = HMGT.db and HMGT.db.profile and HMGT.db.profile.groupCooldownTracker
|
||||
|
||||
if settings and settings.includeSelfFrame == true and ownName then
|
||||
table.insert(players, {
|
||||
name = ownName,
|
||||
class = select(2, UnitClass("player")),
|
||||
unitId = "player",
|
||||
isOwn = true,
|
||||
})
|
||||
end
|
||||
|
||||
if IsInGroup() and not IsInRaid() then
|
||||
for i = 1, GetNumGroupMembers() - 1 do
|
||||
local unitId = "party" .. i
|
||||
local name = HMGT:NormalizePlayerName(UnitName(unitId))
|
||||
local class = select(2, UnitClass(unitId))
|
||||
if name and name ~= ownName then
|
||||
table.insert(players, {name = name, class = class, unitId = unitId})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return players
|
||||
end
|
||||
|
||||
|
||||
@@ -42,54 +42,13 @@ function Manager:HidePlayerFrames(frameKey)
|
||||
end
|
||||
|
||||
function Manager:BuildEntriesByPlayerForTracker(tracker)
|
||||
local frameKey = S.GetTrackerFrameKey(tracker.id)
|
||||
local ownName = HMGT:NormalizePlayerName(UnitName("player")) or "Player"
|
||||
if tracker.testMode then
|
||||
local entries = self:CollectTestEntries(tracker)
|
||||
if S.IsGroupTracker(tracker) and tracker.attachToPartyFrame == true then
|
||||
return S.BuildPartyPreviewEntries(entries)
|
||||
return HMGT:BuildEntriesByPlayerForTracker(
|
||||
tracker,
|
||||
self:GetTrackerFrameKey(tracker),
|
||||
function(unitId)
|
||||
return S.ResolveUnitAnchorFrame(unitId)
|
||||
end
|
||||
local byPlayer, order, unitByPlayer = {}, {}, {}
|
||||
if #entries > 0 then
|
||||
byPlayer[ownName] = entries
|
||||
order[1] = ownName
|
||||
unitByPlayer[ownName] = "player"
|
||||
end
|
||||
return byPlayer, order, unitByPlayer, true
|
||||
end
|
||||
if tracker.demoMode then
|
||||
local entries = HMGT:GetDemoEntries(frameKey, S.GetTrackerSpellPool(tracker.categories), tracker)
|
||||
if S.IsGroupTracker(tracker) and tracker.attachToPartyFrame == true then
|
||||
return S.BuildPartyPreviewEntries(entries)
|
||||
end
|
||||
for _, entry in ipairs(entries) do
|
||||
entry.playerName = ownName
|
||||
end
|
||||
local byPlayer, order, unitByPlayer = {}, {}, {}
|
||||
if #entries > 0 then
|
||||
byPlayer[ownName] = entries
|
||||
order[1] = ownName
|
||||
unitByPlayer[ownName] = "player"
|
||||
end
|
||||
return byPlayer, order, unitByPlayer, true
|
||||
end
|
||||
if not tracker.enabled or not HMGT:IsVisibleForCurrentGroup(tracker) then
|
||||
return {}, {}, {}, false
|
||||
end
|
||||
if IsInRaid() or not IsInGroup() then
|
||||
return {}, {}, {}, false
|
||||
end
|
||||
local byPlayer, order, unitByPlayer = {}, {}, {}
|
||||
for _, playerInfo in ipairs(S.GetGroupPlayers(tracker)) do
|
||||
local entries = S.CollectEntriesForPlayer(tracker, playerInfo)
|
||||
if #entries > 0 then
|
||||
local playerName = playerInfo.name
|
||||
byPlayer[playerName] = entries
|
||||
order[#order + 1] = playerName
|
||||
unitByPlayer[playerName] = playerInfo.unitId
|
||||
end
|
||||
end
|
||||
return byPlayer, order, unitByPlayer, true
|
||||
)
|
||||
end
|
||||
|
||||
function Manager:RefreshPerGroupAnchors(tracker, force)
|
||||
@@ -206,11 +165,10 @@ function Manager:UpdatePerGroupMemberTracker(tracker)
|
||||
for _, playerName in ipairs(order) do
|
||||
local frame = self:EnsurePlayerFrame(tracker, playerName)
|
||||
local entries = byPlayer[playerName] or {}
|
||||
if HMGT.FilterDisplayEntries then
|
||||
entries = HMGT:FilterDisplayEntries(tracker, entries) or entries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries)
|
||||
local tickThis = false
|
||||
entries, tickThis = HMGT:FinalizeTrackerEntries(tracker, entries, tracker.trackerKey or nil)
|
||||
if tickThis then
|
||||
shouldTick = true
|
||||
end
|
||||
if #entries > 0 then
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
||||
@@ -219,12 +177,6 @@ function Manager:UpdatePerGroupMemberTracker(tracker)
|
||||
shownOrder[#shownOrder + 1] = playerName
|
||||
shownByPlayer[playerName] = entries
|
||||
entryCount = entryCount + #entries
|
||||
for _, entry in ipairs(entries) do
|
||||
if S.EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
frame:Hide()
|
||||
end
|
||||
|
||||
@@ -97,14 +97,23 @@ HMGT_SpellData.Interrupts = {
|
||||
-- WARLOCK
|
||||
Spell(19647, "Spell Lock", {
|
||||
classes = {"WARLOCK"},
|
||||
specs = {2},
|
||||
specs = {1, 3},
|
||||
category = "interrupt",
|
||||
state = { kind = "cooldown", cooldown = 24 },
|
||||
}),
|
||||
Spell(119914, "Axe Toss", {
|
||||
classes = {"WARLOCK"},
|
||||
specs = {2},
|
||||
category = "interrupt",
|
||||
aliases = { 89766 },
|
||||
petSpellId = 89766,
|
||||
state = { kind = "cooldown", cooldown = 30 },
|
||||
}),
|
||||
Spell(132409, "Spell Lock (Grimoire)", {
|
||||
classes = {"WARLOCK"},
|
||||
specs = {1, 3},
|
||||
category = "interrupt",
|
||||
aliases = { 1276467 },
|
||||
state = { kind = "cooldown", cooldown = 24 },
|
||||
}),
|
||||
|
||||
|
||||
@@ -1,21 +1,44 @@
|
||||
-- Modules/InterruptTracker.lua
|
||||
-- Interrupt tracker based on the shared single-frame tracker base.
|
||||
|
||||
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)
|
||||
|
||||
local Base = HMGT.SingleFrameTrackerBase
|
||||
if not Base then return end
|
||||
local module = HMGT:NewModule("InterruptTracker")
|
||||
HMGT.InterruptTracker = module
|
||||
|
||||
Base:CreateModule("InterruptTracker", {
|
||||
profileKey = "interruptTracker",
|
||||
frameName = "InterruptTracker",
|
||||
module.definition = {
|
||||
moduleName = "InterruptTracker",
|
||||
dbKey = "interruptTracker",
|
||||
trackerType = "normal",
|
||||
trackerKey = "interruptTracker",
|
||||
title = function()
|
||||
return L["IT_TITLE"]
|
||||
end,
|
||||
demoKey = "interruptTracker",
|
||||
database = function()
|
||||
return HMGT_SpellData.Interrupts
|
||||
end,
|
||||
})
|
||||
categories = { "interrupt" },
|
||||
}
|
||||
|
||||
function module:GetDefinition()
|
||||
return self.definition
|
||||
end
|
||||
|
||||
function module:GetSettings()
|
||||
for _, tracker in ipairs(HMGT:GetTrackerConfigs()) do
|
||||
if tracker.trackerKey == self.definition.trackerKey then
|
||||
return tracker
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function module:Enable()
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.Enable then
|
||||
HMGT.TrackerManager:Enable()
|
||||
end
|
||||
end
|
||||
|
||||
function module:Disable()
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.UpdateDisplay then
|
||||
HMGT.TrackerManager:UpdateDisplay()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,66 +3,15 @@ local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT or not HMGT.TrackerManager then return end
|
||||
|
||||
local Manager = HMGT.TrackerManager
|
||||
local S = Manager._shared or {}
|
||||
|
||||
function Manager:CollectEntries(tracker)
|
||||
local entries = {}
|
||||
local players = S.GetGroupPlayers(tracker)
|
||||
for _, playerInfo in ipairs(players) do
|
||||
local playerEntries = S.CollectEntriesForPlayer(tracker, playerInfo)
|
||||
for _, entry in ipairs(playerEntries) do
|
||||
entries[#entries + 1] = entry
|
||||
end
|
||||
end
|
||||
return entries
|
||||
return HMGT:CollectTrackerEntries(tracker)
|
||||
end
|
||||
|
||||
function Manager:CollectTestEntries(tracker)
|
||||
local playerName = HMGT:NormalizePlayerName(UnitName("player")) or "Player"
|
||||
local classToken = select(2, UnitClass("player"))
|
||||
if not classToken then
|
||||
return {}
|
||||
end
|
||||
|
||||
local entries = {}
|
||||
local pData = HMGT.playerData[playerName]
|
||||
local talents = pData and pData.talents or {}
|
||||
local spells = S.GetTrackerSpellsForPlayer(classToken, GetSpecialization() or 0, tracker.categories)
|
||||
for _, spellEntry in ipairs(spells) do
|
||||
if tracker.enabledSpells[spellEntry.spellId] ~= false then
|
||||
local remaining, total, currentCharges, maxCharges = HMGT:GetCooldownInfo(playerName, spellEntry.spellId)
|
||||
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
local isAvailabilitySpell = HMGT:IsAvailabilitySpell(spellEntry)
|
||||
local spellKnown = HMGT:IsTrackedSpellKnownForPlayer(playerName, spellEntry.spellId)
|
||||
local hasPartialCharges = (tonumber(maxCharges) or 0) > 0
|
||||
and (tonumber(currentCharges) or tonumber(maxCharges) or 0) < (tonumber(maxCharges) or 0)
|
||||
local hasActiveCd = ((remaining or 0) > 0) or hasPartialCharges
|
||||
local hasAvailabilityState = isAvailabilitySpell and HMGT:HasAvailabilityState(playerName, spellEntry.spellId)
|
||||
if spellKnown or hasActiveCd or hasAvailabilityState then
|
||||
entries[#entries + 1] = {
|
||||
playerName = playerName,
|
||||
class = classToken,
|
||||
spellEntry = spellEntry,
|
||||
remaining = remaining,
|
||||
total = total > 0 and total or effectiveCd,
|
||||
currentCharges = currentCharges,
|
||||
maxCharges = maxCharges,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
return entries
|
||||
return HMGT:CollectTrackerTestEntries(tracker)
|
||||
end
|
||||
|
||||
function Manager:BuildEntriesForTracker(tracker)
|
||||
if tracker.testMode then
|
||||
return self:CollectTestEntries(tracker), true
|
||||
end
|
||||
if tracker.demoMode then
|
||||
return HMGT:GetDemoEntries(S.GetTrackerFrameKey(tracker.id), S.GetTrackerSpellPool(tracker.categories), tracker), true
|
||||
end
|
||||
if not tracker.enabled or not HMGT:IsVisibleForCurrentGroup(tracker) then
|
||||
return {}, false
|
||||
end
|
||||
return self:CollectEntries(tracker), true
|
||||
return HMGT:BuildEntriesForTracker(tracker, self:GetTrackerFrameKey(tracker))
|
||||
end
|
||||
|
||||
@@ -1,21 +1,44 @@
|
||||
-- Modules/RaidCooldownTracker.lua
|
||||
-- Raid cooldown tracker based on the shared single-frame tracker base.
|
||||
|
||||
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)
|
||||
|
||||
local Base = HMGT.SingleFrameTrackerBase
|
||||
if not Base then return end
|
||||
local module = HMGT:NewModule("RaidCooldownTracker")
|
||||
HMGT.RaidCooldownTracker = module
|
||||
|
||||
Base:CreateModule("RaidCooldownTracker", {
|
||||
profileKey = "raidCooldownTracker",
|
||||
frameName = "RaidCooldownTracker",
|
||||
module.definition = {
|
||||
moduleName = "RaidCooldownTracker",
|
||||
dbKey = "raidCooldownTracker",
|
||||
trackerType = "normal",
|
||||
trackerKey = "raidCooldownTracker",
|
||||
title = function()
|
||||
return L["RCD_TITLE"]
|
||||
end,
|
||||
demoKey = "raidCooldownTracker",
|
||||
database = function()
|
||||
return HMGT_SpellData.RaidCooldowns
|
||||
end,
|
||||
})
|
||||
categories = { "lust", "defensive", "healing", "tank", "utility", "offensive", "cc", "interrupt" },
|
||||
}
|
||||
|
||||
function module:GetDefinition()
|
||||
return self.definition
|
||||
end
|
||||
|
||||
function module:GetSettings()
|
||||
for _, tracker in ipairs(HMGT:GetTrackerConfigs()) do
|
||||
if tracker.trackerKey == self.definition.trackerKey then
|
||||
return tracker
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function module:Enable()
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.Enable then
|
||||
HMGT.TrackerManager:Enable()
|
||||
end
|
||||
end
|
||||
|
||||
function module:Disable()
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.UpdateDisplay then
|
||||
HMGT.TrackerManager:UpdateDisplay()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,305 +0,0 @@
|
||||
-- Modules/Tracker/SingleFrameTrackerBase.lua
|
||||
-- Shared implementation for single-frame tracker modules.
|
||||
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
HMGT.SingleFrameTrackerBase = HMGT.SingleFrameTrackerBase or {}
|
||||
local Base = HMGT.SingleFrameTrackerBase
|
||||
|
||||
local function GetDefaultGroupPlayers()
|
||||
local players = {}
|
||||
|
||||
local ownName = HMGT:NormalizePlayerName(UnitName("player"))
|
||||
local ownClass = select(2, UnitClass("player"))
|
||||
table.insert(players, { name = ownName, class = ownClass, isOwn = true, unitId = "player" })
|
||||
|
||||
if IsInRaid() then
|
||||
for i = 1, GetNumGroupMembers() do
|
||||
local unitId = "raid" .. i
|
||||
local name = HMGT:NormalizePlayerName(UnitName(unitId))
|
||||
local class = select(2, UnitClass(unitId))
|
||||
if name and name ~= ownName then
|
||||
table.insert(players, { name = name, class = class, unitId = unitId })
|
||||
end
|
||||
end
|
||||
elseif IsInGroup() then
|
||||
for i = 1, GetNumGroupMembers() - 1 do
|
||||
local unitId = "party" .. i
|
||||
local name = HMGT:NormalizePlayerName(UnitName(unitId))
|
||||
local class = select(2, UnitClass(unitId))
|
||||
if name and name ~= ownName then
|
||||
table.insert(players, { name = name, class = class, unitId = unitId })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return players
|
||||
end
|
||||
|
||||
local function ResolveConfigValue(configValue, self)
|
||||
if type(configValue) == "function" then
|
||||
return configValue(self)
|
||||
end
|
||||
return configValue
|
||||
end
|
||||
|
||||
local function EntryNeedsVisualTicker(entry)
|
||||
if type(entry) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
local remaining = tonumber(entry.remaining) or 0
|
||||
if remaining > 0 then
|
||||
return true
|
||||
end
|
||||
|
||||
local maxCharges = tonumber(entry.maxCharges) or 0
|
||||
local currentCharges = tonumber(entry.currentCharges)
|
||||
if maxCharges > 0 and currentCharges ~= nil and currentCharges < maxCharges then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function Base:Create(config)
|
||||
local tracker = {
|
||||
frame = nil,
|
||||
updateTicker = nil,
|
||||
lastEntryCount = 0,
|
||||
}
|
||||
|
||||
function tracker:GetSettings()
|
||||
return HMGT.db.profile[config.profileKey]
|
||||
end
|
||||
|
||||
function tracker:GetDatabase()
|
||||
return ResolveConfigValue(config.database, self) or {}
|
||||
end
|
||||
|
||||
function tracker:GetTitle()
|
||||
return ResolveConfigValue(config.title, self) or config.frameName
|
||||
end
|
||||
|
||||
function tracker:GetDemoKey()
|
||||
return ResolveConfigValue(config.demoKey, self) or config.profileKey
|
||||
end
|
||||
|
||||
function tracker:GetCooldownInfoOpts()
|
||||
return ResolveConfigValue(config.cooldownInfoOpts, self)
|
||||
end
|
||||
|
||||
function tracker:GetGroupPlayers()
|
||||
local custom = ResolveConfigValue(config.groupPlayersProvider, self)
|
||||
if type(custom) == "table" then
|
||||
return custom
|
||||
end
|
||||
return GetDefaultGroupPlayers()
|
||||
end
|
||||
|
||||
function tracker:EnsureUpdateTicker()
|
||||
if self.updateTicker then
|
||||
return
|
||||
end
|
||||
self.updateTicker = C_Timer.NewTicker(0.1, function()
|
||||
self:UpdateDisplay()
|
||||
end)
|
||||
end
|
||||
|
||||
function tracker:StopUpdateTicker()
|
||||
if self.updateTicker then
|
||||
self.updateTicker:Cancel()
|
||||
self.updateTicker = nil
|
||||
end
|
||||
end
|
||||
|
||||
function tracker:SetUpdateTickerEnabled(enabled)
|
||||
if enabled then
|
||||
self:EnsureUpdateTicker()
|
||||
else
|
||||
self:StopUpdateTicker()
|
||||
end
|
||||
end
|
||||
|
||||
function tracker:Enable()
|
||||
local s = self:GetSettings()
|
||||
if not s.enabled and not s.demoMode and not s.testMode then return end
|
||||
|
||||
if not self.frame then
|
||||
self.frame = HMGT.TrackerFrame:CreateTrackerFrame(config.frameName, s)
|
||||
HMGT.TrackerFrame:SetTitle(self.frame, self:GetTitle())
|
||||
end
|
||||
|
||||
if HMGT:IsVisibleForCurrentGroup(s) then
|
||||
self.frame:Show()
|
||||
else
|
||||
self.frame:Hide()
|
||||
end
|
||||
self:UpdateDisplay()
|
||||
end
|
||||
|
||||
function tracker:Disable()
|
||||
self:StopUpdateTicker()
|
||||
if self.frame then
|
||||
self.frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
function tracker:UpdateDisplay()
|
||||
if not self.frame then
|
||||
self:StopUpdateTicker()
|
||||
return
|
||||
end
|
||||
|
||||
local s = self:GetSettings()
|
||||
local database = self:GetDatabase()
|
||||
local cooldownInfoOpts = self:GetCooldownInfoOpts()
|
||||
|
||||
if s.testMode then
|
||||
HMGT.TrackerFrame:SetLocked(self.frame, s.locked)
|
||||
local entries = HMGT:GetOwnTestEntries(database, s, cooldownInfoOpts)
|
||||
self.lastEntryCount = #entries
|
||||
HMGT.TrackerFrame:UpdateFrame(self.frame, entries)
|
||||
self.frame:Show()
|
||||
local shouldTick = false
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
self:SetUpdateTickerEnabled(shouldTick)
|
||||
return
|
||||
end
|
||||
|
||||
if s.demoMode then
|
||||
HMGT.TrackerFrame:SetLocked(self.frame, s.locked)
|
||||
local entries = HMGT:GetDemoEntries(self:GetDemoKey(), database, s)
|
||||
self.lastEntryCount = #entries
|
||||
HMGT.TrackerFrame:UpdateFrame(self.frame, entries)
|
||||
self.frame:Show()
|
||||
self:SetUpdateTickerEnabled(#entries > 0)
|
||||
return
|
||||
end
|
||||
|
||||
if not s.enabled then
|
||||
self.lastEntryCount = 0
|
||||
self.frame:Hide()
|
||||
self:StopUpdateTicker()
|
||||
return
|
||||
end
|
||||
|
||||
if not HMGT:IsVisibleForCurrentGroup(s) then
|
||||
self.lastEntryCount = 0
|
||||
self.frame:Hide()
|
||||
self:StopUpdateTicker()
|
||||
return
|
||||
end
|
||||
|
||||
HMGT.TrackerFrame:SetLocked(self.frame, s.locked)
|
||||
|
||||
local entries = self:CollectEntries()
|
||||
self.lastEntryCount = #entries
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries, config.profileKey)
|
||||
end
|
||||
|
||||
HMGT.TrackerFrame:UpdateFrame(self.frame, entries)
|
||||
self.frame:Show()
|
||||
|
||||
local shouldTick = false
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
self:SetUpdateTickerEnabled(shouldTick)
|
||||
end
|
||||
|
||||
function tracker:CollectEntries()
|
||||
local entries = {}
|
||||
local s = self:GetSettings()
|
||||
local database = self:GetDatabase()
|
||||
local cooldownInfoOpts = self:GetCooldownInfoOpts()
|
||||
local players = self:GetGroupPlayers()
|
||||
|
||||
for _, playerInfo in ipairs(players) do
|
||||
repeat
|
||||
local name = playerInfo.name
|
||||
local pData = HMGT.playerData[name]
|
||||
local class = pData and pData.class or playerInfo.class
|
||||
local specIdx
|
||||
if playerInfo.isOwn then
|
||||
specIdx = GetSpecialization()
|
||||
if not specIdx or specIdx == 0 then break end
|
||||
else
|
||||
specIdx = pData and pData.specIndex or nil
|
||||
if not specIdx or tonumber(specIdx) <= 0 then break end
|
||||
end
|
||||
local talents = pData and pData.talents or {}
|
||||
|
||||
if not class then break end
|
||||
|
||||
local knownSpells = HMGT_SpellData.GetSpellsForSpec(class, specIdx, database)
|
||||
|
||||
for _, spellEntry in ipairs(knownSpells) do
|
||||
if s.enabledSpells[spellEntry.spellId] ~= false then
|
||||
local remaining, total, curCharges, maxCharges = HMGT:GetCooldownInfo(name, spellEntry.spellId, cooldownInfoOpts)
|
||||
local isAvailabilitySpell = HMGT.IsAvailabilitySpell and HMGT:IsAvailabilitySpell(spellEntry)
|
||||
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
local include = HMGT:ShouldDisplayEntry(s, remaining, curCharges, maxCharges, spellEntry)
|
||||
local spellKnown = HMGT:IsTrackedSpellKnownForPlayer(name, spellEntry.spellId)
|
||||
local hasPartialCharges = (tonumber(maxCharges) or 0) > 0
|
||||
and (tonumber(curCharges) or tonumber(maxCharges) or 0) < (tonumber(maxCharges) or 0)
|
||||
local hasActiveCd = ((remaining or 0) > 0) or hasPartialCharges
|
||||
if not spellKnown and not hasActiveCd then
|
||||
include = false
|
||||
end
|
||||
if isAvailabilitySpell and not spellKnown then
|
||||
include = false
|
||||
end
|
||||
|
||||
if not playerInfo.isOwn then
|
||||
if isAvailabilitySpell and not HMGT:HasAvailabilityState(name, spellEntry.spellId) then
|
||||
include = false
|
||||
end
|
||||
end
|
||||
|
||||
if include then
|
||||
entries[#entries + 1] = {
|
||||
playerName = name,
|
||||
class = class,
|
||||
spellEntry = spellEntry,
|
||||
remaining = remaining,
|
||||
total = total > 0 and total or effectiveCd,
|
||||
currentCharges = curCharges,
|
||||
maxCharges = maxCharges,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
until true
|
||||
end
|
||||
|
||||
return entries
|
||||
end
|
||||
|
||||
return tracker
|
||||
end
|
||||
|
||||
function Base:CreateModule(moduleName, config, ...)
|
||||
if type(moduleName) ~= "string" or moduleName == "" then
|
||||
return self:Create(config)
|
||||
end
|
||||
|
||||
local module = HMGT:NewModule(moduleName, ...)
|
||||
local tracker = self:Create(config)
|
||||
for key, value in pairs(tracker) do
|
||||
module[key] = value
|
||||
end
|
||||
HMGT[moduleName] = module
|
||||
return module
|
||||
end
|
||||
@@ -1077,6 +1077,18 @@ function HMGT_SpellData.RebuildLookups()
|
||||
for _, entry in ipairs(HMGT_SpellData.Interrupts or {}) do
|
||||
entry._hmgtDataset = "Interrupts"
|
||||
HMGT_SpellData.InterruptLookup[entry.spellId] = entry
|
||||
if type(entry.aliases) == "table" then
|
||||
for _, aliasId in ipairs(entry.aliases) do
|
||||
local sid = tonumber(aliasId)
|
||||
if sid and sid > 0 then
|
||||
HMGT_SpellData.InterruptLookup[sid] = entry
|
||||
end
|
||||
end
|
||||
end
|
||||
local petSpellId = tonumber(entry.petSpellId)
|
||||
if petSpellId and petSpellId > 0 then
|
||||
HMGT_SpellData.InterruptLookup[petSpellId] = entry
|
||||
end
|
||||
end
|
||||
|
||||
HMGT_SpellData.CooldownLookup = {}
|
||||
|
||||
169
Modules/Tracker/TrackerAvailability.lua
Normal file
169
Modules/Tracker/TrackerAvailability.lua
Normal file
@@ -0,0 +1,169 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
HMGT.TrackerAvailability = HMGT.TrackerAvailability or {}
|
||||
|
||||
local internals = HMGT.TrackerInternals or {}
|
||||
local GetPlayerAuraApplications = internals.GetPlayerAuraApplications
|
||||
local GetSpellCastCountInfo = internals.GetSpellCastCountInfo
|
||||
|
||||
function HMGT:GetOwnAvailabilityProgress(spellEntry)
|
||||
local availability = self:GetAvailabilityConfig(spellEntry)
|
||||
if not availability then
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
local required = self:GetAvailabilityRequiredCount(spellEntry)
|
||||
if required <= 0 then
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
local current = 0
|
||||
if availability.type == "auraStacks" then
|
||||
current = GetPlayerAuraApplications and GetPlayerAuraApplications(availability.auraSpellId) or 0
|
||||
if current <= 0 then
|
||||
local fallbackSpellId = tonumber(availability.fallbackSpellCountId)
|
||||
or tonumber(availability.progressSpellId)
|
||||
or tonumber(spellEntry and spellEntry.spellId)
|
||||
if fallbackSpellId and fallbackSpellId > 0 and GetSpellCastCountInfo then
|
||||
current = GetSpellCastCountInfo(fallbackSpellId)
|
||||
end
|
||||
end
|
||||
else
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
current = math.max(0, math.min(required, tonumber(current) or 0))
|
||||
return current, required
|
||||
end
|
||||
|
||||
function HMGT:GetAvailabilityState(playerName, spellId)
|
||||
local state = self:GetAvailabilityStateEntry(playerName, spellId)
|
||||
if not state then
|
||||
return nil, nil
|
||||
end
|
||||
return tonumber(state.current) or 0, tonumber(state.max) or 0
|
||||
end
|
||||
|
||||
function HMGT:HasAvailabilityState(playerName, spellId)
|
||||
local _, max = self:GetAvailabilityState(playerName, spellId)
|
||||
return (tonumber(max) or 0) > 0
|
||||
end
|
||||
|
||||
function HMGT:StoreAvailabilityState(playerName, spellId, current, max, spellEntry)
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
local sid = tonumber(spellId)
|
||||
if not normalizedName or not sid or sid <= 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
local maxCount = math.max(0, math.floor((tonumber(max) or 0) + 0.5))
|
||||
if maxCount <= 0 then
|
||||
return self:ClearAvailabilityState(normalizedName, sid)
|
||||
end
|
||||
|
||||
local currentCount = math.max(0, math.min(maxCount, math.floor((tonumber(current) or 0) + 0.5)))
|
||||
local previous = self:GetAvailabilityStateEntry(normalizedName, sid)
|
||||
local changed = (not previous)
|
||||
or (tonumber(previous.current) or -1) ~= currentCount
|
||||
or (tonumber(previous.max) or -1) ~= maxCount
|
||||
|
||||
self:SetAvailabilityStateEntry(normalizedName, sid, {
|
||||
current = currentCount,
|
||||
max = maxCount,
|
||||
spellEntry = spellEntry,
|
||||
updatedAt = GetTime(),
|
||||
})
|
||||
|
||||
return changed
|
||||
end
|
||||
|
||||
function HMGT:RefreshOwnAvailabilitySpell(spellEntry)
|
||||
if not self:IsAvailabilitySpell(spellEntry) then
|
||||
return false
|
||||
end
|
||||
|
||||
local playerName = self:NormalizePlayerName(UnitName("player"))
|
||||
if not playerName then
|
||||
return false
|
||||
end
|
||||
|
||||
local current, max = self:GetOwnAvailabilityProgress(spellEntry)
|
||||
if (tonumber(max) or 0) > 0 then
|
||||
local pData = self.playerData[playerName]
|
||||
if pData and type(pData.knownSpells) == "table" then
|
||||
pData.knownSpells[tonumber(spellEntry.spellId)] = true
|
||||
end
|
||||
end
|
||||
return self:StoreAvailabilityState(playerName, spellEntry.spellId, current, max, spellEntry)
|
||||
end
|
||||
|
||||
function HMGT:RefreshOwnAvailabilityStates()
|
||||
local playerName = self:NormalizePlayerName(UnitName("player"))
|
||||
local pData = playerName and self.playerData[playerName]
|
||||
if not pData or not pData.class or not pData.specIndex then
|
||||
return false
|
||||
end
|
||||
|
||||
local changed = false
|
||||
local groupCooldowns = HMGT_SpellData.GetSpellsForSpec(pData.class, pData.specIndex, HMGT_SpellData.GroupCooldowns)
|
||||
for _, spellEntry in ipairs(groupCooldowns or {}) do
|
||||
if self:IsAvailabilitySpell(spellEntry) and self:RefreshOwnAvailabilitySpell(spellEntry) then
|
||||
changed = true
|
||||
end
|
||||
end
|
||||
|
||||
if self:PruneAvailabilityStates(playerName, pData.knownSpells or {}) then
|
||||
changed = true
|
||||
end
|
||||
|
||||
return changed
|
||||
end
|
||||
|
||||
function HMGT:RefreshAndPublishOwnAvailabilityStates()
|
||||
local playerName = self:NormalizePlayerName(UnitName("player"))
|
||||
local pData = playerName and self.playerData[playerName]
|
||||
if not pData or not pData.class or not pData.specIndex then
|
||||
return false
|
||||
end
|
||||
|
||||
local changed = false
|
||||
local groupCooldowns = HMGT_SpellData.GetSpellsForSpec(pData.class, pData.specIndex, HMGT_SpellData.GroupCooldowns)
|
||||
for _, spellEntry in ipairs(groupCooldowns or {}) do
|
||||
if self:IsAvailabilitySpell(spellEntry) and self:RefreshOwnAvailabilitySpell(spellEntry) then
|
||||
self:PublishOwnSpellState(spellEntry.spellId, { sendLegacy = true })
|
||||
changed = true
|
||||
end
|
||||
end
|
||||
|
||||
if self:PruneAvailabilityStates(playerName, pData.knownSpells or {}) then
|
||||
changed = true
|
||||
end
|
||||
|
||||
return changed
|
||||
end
|
||||
|
||||
function HMGT:SendOwnAvailabilityStates(target)
|
||||
local playerName = self:NormalizePlayerName(UnitName("player"))
|
||||
local pData = playerName and self.playerData[playerName]
|
||||
if not pData or not pData.class or not pData.specIndex then
|
||||
return 0
|
||||
end
|
||||
|
||||
self:RefreshOwnAvailabilityStates()
|
||||
|
||||
local sent = 0
|
||||
local groupCooldowns = HMGT_SpellData.GetSpellsForSpec(pData.class, pData.specIndex, HMGT_SpellData.GroupCooldowns)
|
||||
for _, spellEntry in ipairs(groupCooldowns or {}) do
|
||||
if self:IsAvailabilitySpell(spellEntry) and self:IsTrackedSpellKnownForPlayer(playerName, spellEntry.spellId) then
|
||||
local current, max = self:GetAvailabilityState(playerName, spellEntry.spellId)
|
||||
if (tonumber(max) or 0) > 0 then
|
||||
self:BroadcastAvailabilityState(spellEntry.spellId, current, max, target)
|
||||
sent = sent + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return sent
|
||||
end
|
||||
191
Modules/Tracker/TrackerBridge.lua
Normal file
191
Modules/Tracker/TrackerBridge.lua
Normal file
@@ -0,0 +1,191 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
HMGT.TrackerBridge = HMGT.TrackerBridge or {}
|
||||
|
||||
function HMGT:RegisterExternalAddonSource(sourceName)
|
||||
local source = tostring(sourceName or "")
|
||||
if source == "" then
|
||||
return false
|
||||
end
|
||||
self.externalAddonSources = self.externalAddonSources or {}
|
||||
self.externalAddonSources[source] = true
|
||||
return true
|
||||
end
|
||||
|
||||
function HMGT:GetCanonicalExternalSpellEntry(spellId)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid or sid <= 0 or not HMGT_SpellData then
|
||||
return nil, sid
|
||||
end
|
||||
|
||||
local spellEntry = HMGT_SpellData.InterruptLookup and HMGT_SpellData.InterruptLookup[sid]
|
||||
or HMGT_SpellData.CooldownLookup and HMGT_SpellData.CooldownLookup[sid]
|
||||
if not spellEntry then
|
||||
return nil, sid
|
||||
end
|
||||
|
||||
return spellEntry, tonumber(spellEntry.spellId) or sid
|
||||
end
|
||||
|
||||
function HMGT:InferClassFromSpellEntry(spellEntry)
|
||||
if type(spellEntry) ~= "table" or type(spellEntry.classes) ~= "table" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local foundClass
|
||||
for key, value in pairs(spellEntry.classes) do
|
||||
local classToken = type(value) == "string" and value or key
|
||||
if foundClass and foundClass ~= classToken then
|
||||
return nil
|
||||
end
|
||||
foundClass = classToken
|
||||
end
|
||||
return foundClass
|
||||
end
|
||||
|
||||
function HMGT:ApplyExternalKnownSpell(sourceName, playerName, spellId, class, cooldown)
|
||||
local source = tostring(sourceName or "External")
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
local sid = tonumber(spellId)
|
||||
if not normalizedName or normalizedName == "" or not sid or sid <= 0 then
|
||||
return false, "invalid_args"
|
||||
end
|
||||
if not self:IsPlayerInCurrentGroup(normalizedName) then
|
||||
return false, "not_in_group"
|
||||
end
|
||||
|
||||
local spellEntry, canonicalSid = self:GetCanonicalExternalSpellEntry(sid)
|
||||
if not spellEntry or not canonicalSid or canonicalSid <= 0 then
|
||||
return false, "unknown_spell"
|
||||
end
|
||||
sid = canonicalSid
|
||||
|
||||
self:RegisterExternalAddonSource(source)
|
||||
local previous = self.playerData[normalizedName] or {}
|
||||
local knownSpells = previous.knownSpells
|
||||
if type(knownSpells) ~= "table" then
|
||||
knownSpells = {}
|
||||
end
|
||||
knownSpells[sid] = true
|
||||
|
||||
local classToken = class or previous.class or self:InferClassFromSpellEntry(spellEntry)
|
||||
|
||||
self.playerData[normalizedName] = {
|
||||
class = classToken,
|
||||
specIndex = previous.specIndex,
|
||||
talentHash = previous.talentHash,
|
||||
talents = previous.talents or {},
|
||||
knownSpells = knownSpells,
|
||||
externalSource = source,
|
||||
}
|
||||
self:SetPlayerBridgeStatus(normalizedName, source)
|
||||
self:DebugScoped("verbose", "TrackerBridge", "Bridge known spell source=%s player=%s spellId=%s", tostring(source), tostring(normalizedName), tostring(sid))
|
||||
|
||||
if tonumber(cooldown) and tonumber(cooldown) > 0 then
|
||||
spellEntry._hmgtExternalBaseCd = tonumber(cooldown)
|
||||
end
|
||||
|
||||
self:TriggerTrackerUpdate("trackers")
|
||||
return true
|
||||
end
|
||||
|
||||
function HMGT:ApplyExternalSpecInfo(sourceName, playerName, class, specId, talentHash)
|
||||
local source = tostring(sourceName or "External")
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
local spec = tonumber(specId)
|
||||
local classToken = class and tostring(class) or self:GetClassTokenForSpecId(spec)
|
||||
if not normalizedName or normalizedName == "" or not classToken or classToken == "" or not spec or spec <= 0 then
|
||||
return false, "invalid_args"
|
||||
end
|
||||
if not self:IsPlayerInCurrentGroup(normalizedName) then
|
||||
return false, "not_in_group"
|
||||
end
|
||||
|
||||
self:RegisterExternalAddonSource(source)
|
||||
local previous = self.playerData[normalizedName] or {}
|
||||
local knownSpells = previous.knownSpells
|
||||
if type(knownSpells) ~= "table" then
|
||||
knownSpells = {}
|
||||
end
|
||||
|
||||
if HMGT_SpellData and type(HMGT_SpellData.GetSpellsForSpec) == "function" then
|
||||
for _, datasetName in ipairs({ "Interrupts", "RaidCooldowns", "GroupCooldowns" }) do
|
||||
local dataset = HMGT_SpellData[datasetName]
|
||||
for _, spellEntry in ipairs(HMGT_SpellData.GetSpellsForSpec(classToken, spec, dataset)) do
|
||||
local sid = tonumber(spellEntry and spellEntry.spellId)
|
||||
if sid and sid > 0 then
|
||||
knownSpells[sid] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self.playerData[normalizedName] = {
|
||||
class = classToken,
|
||||
specIndex = spec,
|
||||
talentHash = talentHash or previous.talentHash,
|
||||
talents = self:ParseTalentHash(talentHash or previous.talentHash),
|
||||
knownSpells = knownSpells,
|
||||
externalSource = source,
|
||||
}
|
||||
self:SetPlayerBridgeStatus(normalizedName, source)
|
||||
self:DebugScoped("info", "TrackerBridge", "Bridge spec sync source=%s player=%s class=%s spec=%s", tostring(source), tostring(normalizedName), tostring(classToken), tostring(spec))
|
||||
|
||||
self:PruneAvailabilityStates(normalizedName, knownSpells)
|
||||
self:TriggerTrackerUpdate("trackers")
|
||||
return true
|
||||
end
|
||||
|
||||
function HMGT:ApplyExternalCooldown(sourceName, playerName, spellId, cooldown)
|
||||
local source = tostring(sourceName or "External")
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
local sid = tonumber(spellId)
|
||||
local cd = tonumber(cooldown)
|
||||
if not normalizedName or normalizedName == "" or not sid or sid <= 0 or not cd or cd <= 0 then
|
||||
return false, "invalid_args"
|
||||
end
|
||||
if not self:IsPlayerInCurrentGroup(normalizedName) then
|
||||
return false, "not_in_group"
|
||||
end
|
||||
|
||||
local spellEntry, canonicalSid = self:GetCanonicalExternalSpellEntry(sid)
|
||||
if not spellEntry or not canonicalSid or canonicalSid <= 0 then
|
||||
return false, "unknown_spell"
|
||||
end
|
||||
sid = canonicalSid
|
||||
|
||||
self:RegisterExternalAddonSource(source)
|
||||
self:ApplyExternalKnownSpell(source, normalizedName, sid, nil, cd)
|
||||
self:DebugScoped("info", "TrackerBridge", "Bridge cooldown source=%s player=%s spellId=%s cooldown=%.1f", tostring(source), tostring(normalizedName), tostring(sid), cd)
|
||||
self:HandleRemoteSpellCast(normalizedName, sid, GetServerTime(), nil, nil, nil, cd)
|
||||
return true
|
||||
end
|
||||
|
||||
function HMGT:IsPlayerInCurrentGroup(playerName)
|
||||
local target = self:NormalizePlayerName(playerName)
|
||||
if not target then return false end
|
||||
local own = self:NormalizePlayerName(UnitName("player"))
|
||||
if target == own then return true end
|
||||
|
||||
if IsInRaid() then
|
||||
for i = 1, GetNumGroupMembers() do
|
||||
local n = self:NormalizePlayerName(UnitName("raid" .. i))
|
||||
if n == target then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
if IsInGroup() then
|
||||
for i = 1, GetNumSubgroupMembers() do
|
||||
local n = self:NormalizePlayerName(UnitName("party" .. i))
|
||||
if n == target then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
576
Modules/Tracker/TrackerCore.lua
Normal file
576
Modules/Tracker/TrackerCore.lua
Normal file
@@ -0,0 +1,576 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
HMGT.TrackerCore = HMGT.TrackerCore or {}
|
||||
|
||||
HMGT.TRACKER_PRESET_DEFINITIONS = HMGT.TRACKER_PRESET_DEFINITIONS or {
|
||||
interruptTracker = {
|
||||
moduleName = "InterruptTracker",
|
||||
dbKey = "interruptTracker",
|
||||
trackerType = "normal",
|
||||
trackerKey = "interruptTracker",
|
||||
categories = { "interrupt" },
|
||||
defaultName = function(L)
|
||||
return (L and L["IT_NAME"]) or "Interrupts"
|
||||
end,
|
||||
},
|
||||
raidCooldownTracker = {
|
||||
moduleName = "RaidCooldownTracker",
|
||||
dbKey = "raidCooldownTracker",
|
||||
trackerType = "normal",
|
||||
trackerKey = "raidCooldownTracker",
|
||||
categories = { "lust", "defensive", "healing", "tank", "utility", "offensive", "cc", "interrupt" },
|
||||
defaultName = function(L)
|
||||
return (L and L["RCD_NAME"]) or "Raid Cooldowns"
|
||||
end,
|
||||
},
|
||||
groupCooldownTracker = {
|
||||
moduleName = "GroupCooldownTracker",
|
||||
dbKey = "groupCooldownTracker",
|
||||
trackerType = "group",
|
||||
trackerKey = "groupCooldownTracker",
|
||||
categories = { "tank", "defensive", "healing", "cc", "utility", "offensive", "lust", "interrupt" },
|
||||
includeSelfFrame = false,
|
||||
showChargesOnIcon = true,
|
||||
defaultName = function(L)
|
||||
return (L and L["GCD_NAME"]) or "Cooldowns"
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
||||
function HMGT:GetTrackerPresetDefinitions()
|
||||
return self.TRACKER_PRESET_DEFINITIONS or {}
|
||||
end
|
||||
|
||||
function HMGT:GetTrackerPresetDefinition(key)
|
||||
local definitions = self:GetTrackerPresetDefinitions()
|
||||
return definitions and definitions[tostring(key or "")]
|
||||
end
|
||||
|
||||
function HMGT:GetTrackerPresetDefinitionByModule(moduleName)
|
||||
local target = tostring(moduleName or "")
|
||||
for _, definition in pairs(self:GetTrackerPresetDefinitions()) do
|
||||
if tostring(definition.moduleName or "") == target then
|
||||
return definition
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function HMGT:GetTrackerTypeOptions()
|
||||
local L = self.L
|
||||
return {
|
||||
normal = (L and L["OPT_TRACKER_TYPE_NORMAL"]) or "Normal tracker",
|
||||
group = (L and L["OPT_TRACKER_TYPE_GROUP"]) or "Group-based tracker",
|
||||
}
|
||||
end
|
||||
|
||||
function HMGT:BuildTrackerConfigFromPreset(presetKey, trackerId, overrides)
|
||||
local definition = self:GetTrackerPresetDefinition(presetKey)
|
||||
local config = overrides or {}
|
||||
if not definition then
|
||||
return self:CreateTrackerConfig(trackerId, config)
|
||||
end
|
||||
|
||||
local base = {
|
||||
name = type(definition.defaultName) == "function" and definition.defaultName(self.L) or tostring(definition.defaultName or ""),
|
||||
trackerType = definition.trackerType,
|
||||
trackerKey = definition.trackerKey,
|
||||
categories = definition.categories,
|
||||
includeSelfFrame = definition.includeSelfFrame,
|
||||
showChargesOnIcon = definition.showChargesOnIcon,
|
||||
}
|
||||
|
||||
for key, value in pairs(config) do
|
||||
base[key] = value
|
||||
end
|
||||
|
||||
return self:CreateTrackerConfig(trackerId, base)
|
||||
end
|
||||
|
||||
local function EntryNeedsVisualTicker(entry)
|
||||
if type(entry) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
local remaining = tonumber(entry.remaining) or 0
|
||||
if remaining > 0 then
|
||||
return true
|
||||
end
|
||||
|
||||
local maxCharges = tonumber(entry.maxCharges) or 0
|
||||
local currentCharges = tonumber(entry.currentCharges)
|
||||
if maxCharges > 0 and currentCharges ~= nil and currentCharges < maxCharges then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function HMGT:IsGroupTrackerConfig(tracker)
|
||||
return type(tracker) == "table" and tracker.trackerType == "group"
|
||||
end
|
||||
|
||||
function HMGT:GetTrackerSpellPool(categories)
|
||||
if HMGT_SpellData and type(HMGT_SpellData.GetSpellPoolForCategories) == "function" then
|
||||
return HMGT_SpellData.GetSpellPoolForCategories(categories)
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
function HMGT:GetTrackerSpellsForPlayer(classToken, specIndex, categories)
|
||||
if HMGT_SpellData and type(HMGT_SpellData.GetSpellsForCategories) == "function" then
|
||||
return HMGT_SpellData.GetSpellsForCategories(classToken, specIndex, categories)
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
function HMGT:GetTrackerPlayers(tracker)
|
||||
local players = {}
|
||||
|
||||
local ownName = self:NormalizePlayerName(UnitName("player"))
|
||||
local ownClass = select(2, UnitClass("player"))
|
||||
local includeOwnPlayer = true
|
||||
if self:IsGroupTrackerConfig(tracker) then
|
||||
includeOwnPlayer = tracker.includeSelfFrame == true
|
||||
end
|
||||
if includeOwnPlayer then
|
||||
players[#players + 1] = {
|
||||
name = ownName,
|
||||
class = ownClass,
|
||||
isOwn = true,
|
||||
unitId = "player",
|
||||
}
|
||||
end
|
||||
|
||||
if IsInRaid() then
|
||||
for i = 1, GetNumGroupMembers() do
|
||||
local unitId = "raid" .. i
|
||||
local name = self:NormalizePlayerName(UnitName(unitId))
|
||||
local class = select(2, UnitClass(unitId))
|
||||
if name and name ~= ownName then
|
||||
players[#players + 1] = {
|
||||
name = name,
|
||||
class = class,
|
||||
unitId = unitId,
|
||||
}
|
||||
end
|
||||
end
|
||||
elseif IsInGroup() then
|
||||
for i = 1, GetNumGroupMembers() - 1 do
|
||||
local unitId = "party" .. i
|
||||
local name = self:NormalizePlayerName(UnitName(unitId))
|
||||
local class = select(2, UnitClass(unitId))
|
||||
if name and name ~= ownName then
|
||||
players[#players + 1] = {
|
||||
name = name,
|
||||
class = class,
|
||||
unitId = unitId,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return players
|
||||
end
|
||||
|
||||
function HMGT:CollectTrackerEntriesForPlayer(tracker, playerInfo)
|
||||
local entries = {}
|
||||
if type(tracker) ~= "table" or type(playerInfo) ~= "table" then
|
||||
return entries
|
||||
end
|
||||
|
||||
local playerName = playerInfo.name
|
||||
if not playerName then
|
||||
return entries
|
||||
end
|
||||
|
||||
local pData = self.playerData[playerName]
|
||||
local classToken = pData and pData.class or playerInfo.class
|
||||
if not classToken then
|
||||
return entries
|
||||
end
|
||||
|
||||
local specIndex
|
||||
if playerInfo.isOwn then
|
||||
specIndex = GetSpecialization()
|
||||
if not specIndex or specIndex == 0 then
|
||||
return entries
|
||||
end
|
||||
else
|
||||
specIndex = pData and pData.specIndex or nil
|
||||
if not specIndex or tonumber(specIndex) <= 0 then
|
||||
return entries
|
||||
end
|
||||
end
|
||||
|
||||
local talents = pData and pData.talents or {}
|
||||
local spells = self:GetTrackerSpellsForPlayer(classToken, specIndex, tracker.categories)
|
||||
for _, spellEntry in ipairs(spells) do
|
||||
if tracker.enabledSpells[spellEntry.spellId] ~= false then
|
||||
local remaining, total, currentCharges, maxCharges = self:GetCooldownInfo(playerName, spellEntry.spellId)
|
||||
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
local isAvailabilitySpell = self:IsAvailabilitySpell(spellEntry)
|
||||
local include = self:ShouldDisplayEntry(tracker, remaining, currentCharges, maxCharges, spellEntry)
|
||||
local spellKnown = self:IsTrackedSpellKnownForPlayer(playerName, spellEntry.spellId)
|
||||
local hasPartialCharges = (tonumber(maxCharges) or 0) > 0
|
||||
and (tonumber(currentCharges) or tonumber(maxCharges) or 0) < (tonumber(maxCharges) or 0)
|
||||
local hasActiveCd = ((remaining or 0) > 0) or hasPartialCharges
|
||||
|
||||
if not spellKnown and not hasActiveCd then
|
||||
include = false
|
||||
end
|
||||
if isAvailabilitySpell and not spellKnown then
|
||||
include = false
|
||||
end
|
||||
if not playerInfo.isOwn and isAvailabilitySpell and not self:HasAvailabilityState(playerName, spellEntry.spellId) then
|
||||
include = false
|
||||
end
|
||||
|
||||
if include then
|
||||
entries[#entries + 1] = {
|
||||
playerName = playerName,
|
||||
class = classToken,
|
||||
spellEntry = spellEntry,
|
||||
remaining = remaining,
|
||||
total = total > 0 and total or effectiveCd,
|
||||
currentCharges = currentCharges,
|
||||
maxCharges = maxCharges,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return entries
|
||||
end
|
||||
|
||||
local function CopyEntriesForPreview(entries, playerName)
|
||||
local copies = {}
|
||||
for _, entry in ipairs(entries or {}) do
|
||||
local nextEntry = {}
|
||||
for key, value in pairs(entry) do
|
||||
nextEntry[key] = value
|
||||
end
|
||||
nextEntry.playerName = playerName
|
||||
copies[#copies + 1] = nextEntry
|
||||
end
|
||||
return copies
|
||||
end
|
||||
|
||||
function HMGT:BuildPartyPreviewEntries(entries, resolveUnitAnchorFrame)
|
||||
local byPlayer = {}
|
||||
local order = {}
|
||||
local unitByPlayer = {}
|
||||
for index = 1, 4 do
|
||||
local unitId = "party" .. index
|
||||
if not resolveUnitAnchorFrame or resolveUnitAnchorFrame(unitId) then
|
||||
local playerName = string.format("Party %d", index)
|
||||
local playerEntries = CopyEntriesForPreview(entries, playerName)
|
||||
if #playerEntries > 0 then
|
||||
byPlayer[playerName] = playerEntries
|
||||
order[#order + 1] = playerName
|
||||
unitByPlayer[playerName] = unitId
|
||||
end
|
||||
end
|
||||
end
|
||||
return byPlayer, order, unitByPlayer, #order > 0
|
||||
end
|
||||
|
||||
function HMGT:CollectTrackerEntries(tracker)
|
||||
local entries = {}
|
||||
local players = self:GetTrackerPlayers(tracker)
|
||||
for _, playerInfo in ipairs(players) do
|
||||
local playerEntries = self:CollectTrackerEntriesForPlayer(tracker, playerInfo)
|
||||
for _, entry in ipairs(playerEntries) do
|
||||
entries[#entries + 1] = entry
|
||||
end
|
||||
end
|
||||
return entries
|
||||
end
|
||||
|
||||
function HMGT:GetDemoEntries(trackerKey, database, settings)
|
||||
local pool = {}
|
||||
local poolByClass = {}
|
||||
for _, entry in ipairs(database or {}) do
|
||||
if settings.enabledSpells[entry.spellId] ~= false then
|
||||
pool[#pool + 1] = entry
|
||||
for _, classToken in ipairs(entry.classes or {}) do
|
||||
poolByClass[classToken] = poolByClass[classToken] or {}
|
||||
poolByClass[classToken][#poolByClass[classToken] + 1] = entry
|
||||
end
|
||||
end
|
||||
end
|
||||
if #pool == 0 then
|
||||
return {}
|
||||
end
|
||||
|
||||
local classKeys = {}
|
||||
for classToken in pairs(poolByClass) do
|
||||
classKeys[#classKeys + 1] = classToken
|
||||
end
|
||||
if #classKeys == 0 then
|
||||
classKeys[1] = "WARRIOR"
|
||||
end
|
||||
|
||||
local count = settings.showBar and math.min(8, #pool) or math.min(12, #pool)
|
||||
local names = { "Alice", "Bob", "Clara", "Duke", "Elli", "Fynn", "Gina", "Hektor", "Ivo", "Jana", "Kira", "Lio" }
|
||||
|
||||
local spellIds = {}
|
||||
for _, entry in ipairs(pool) do
|
||||
spellIds[#spellIds + 1] = tostring(entry.spellId)
|
||||
end
|
||||
table.sort(spellIds)
|
||||
local signature = table.concat(spellIds, ",") .. "|" .. tostring(settings.showBar and 1 or 0) .. "|" .. tostring(count)
|
||||
|
||||
local now = GetTime()
|
||||
local cache = self.demoModeData[trackerKey]
|
||||
if (not cache) or cache.signature ~= signature or (not cache.entries) or #cache.entries ~= count then
|
||||
local cachedEntries = {}
|
||||
for index = 1, count do
|
||||
local classToken = classKeys[math.random(1, #classKeys)]
|
||||
local classPool = poolByClass[classToken]
|
||||
local spellEntry = (classPool and classPool[math.random(1, #classPool)]) or pool[math.random(1, #pool)]
|
||||
local duration = math.max(
|
||||
1,
|
||||
tonumber(HMGT_SpellData.GetBaseCooldown and HMGT_SpellData.GetBaseCooldown(spellEntry)) or tonumber(spellEntry.cooldown) or 60
|
||||
)
|
||||
local offset = math.random() * math.min(duration * 0.85, duration - 0.1)
|
||||
cachedEntries[#cachedEntries + 1] = {
|
||||
playerName = names[((index - 1) % #names) + 1],
|
||||
class = classToken or ((spellEntry.classes and spellEntry.classes[1]) or "WARRIOR"),
|
||||
spellEntry = spellEntry,
|
||||
total = duration,
|
||||
cycleStart = now - offset,
|
||||
currentCharges = nil,
|
||||
maxCharges = nil,
|
||||
}
|
||||
end
|
||||
cache = {
|
||||
signature = signature,
|
||||
entries = cachedEntries,
|
||||
}
|
||||
self.demoModeData[trackerKey] = cache
|
||||
end
|
||||
|
||||
local entries = {}
|
||||
for _, entry in ipairs(cache.entries) do
|
||||
local total = math.max(1, tonumber(entry.total) or 1)
|
||||
local elapsed = math.max(0, now - (entry.cycleStart or now))
|
||||
local phase = math.fmod(elapsed, total)
|
||||
local remaining = total - phase
|
||||
if elapsed > 0 and phase < 0.05 then
|
||||
remaining = 0
|
||||
end
|
||||
entries[#entries + 1] = {
|
||||
playerName = entry.playerName,
|
||||
class = entry.class,
|
||||
spellEntry = entry.spellEntry,
|
||||
remaining = remaining,
|
||||
total = total,
|
||||
currentCharges = entry.currentCharges,
|
||||
maxCharges = entry.maxCharges,
|
||||
}
|
||||
end
|
||||
|
||||
return entries
|
||||
end
|
||||
|
||||
function HMGT:CollectTrackerTestEntries(tracker)
|
||||
local playerName = self:NormalizePlayerName(UnitName("player")) or "Player"
|
||||
local classToken = select(2, UnitClass("player"))
|
||||
if not classToken then
|
||||
return {}
|
||||
end
|
||||
|
||||
local entries = {}
|
||||
local pData = self.playerData[playerName]
|
||||
local talents = pData and pData.talents or {}
|
||||
local spells = self:GetTrackerSpellsForPlayer(classToken, GetSpecialization() or 0, tracker.categories)
|
||||
for _, spellEntry in ipairs(spells) do
|
||||
if tracker.enabledSpells[spellEntry.spellId] ~= false then
|
||||
local remaining, total, currentCharges, maxCharges = self:GetCooldownInfo(playerName, spellEntry.spellId)
|
||||
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
local isAvailabilitySpell = self:IsAvailabilitySpell(spellEntry)
|
||||
local spellKnown = self:IsTrackedSpellKnownForPlayer(playerName, spellEntry.spellId)
|
||||
local hasPartialCharges = (tonumber(maxCharges) or 0) > 0
|
||||
and (tonumber(currentCharges) or tonumber(maxCharges) or 0) < (tonumber(maxCharges) or 0)
|
||||
local hasActiveCd = ((remaining or 0) > 0) or hasPartialCharges
|
||||
local hasAvailabilityState = isAvailabilitySpell and self:HasAvailabilityState(playerName, spellEntry.spellId)
|
||||
if spellKnown or hasActiveCd or hasAvailabilityState then
|
||||
entries[#entries + 1] = {
|
||||
playerName = playerName,
|
||||
class = classToken,
|
||||
spellEntry = spellEntry,
|
||||
remaining = remaining,
|
||||
total = total > 0 and total or effectiveCd,
|
||||
currentCharges = currentCharges,
|
||||
maxCharges = maxCharges,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
return entries
|
||||
end
|
||||
|
||||
function HMGT:BuildEntriesForTracker(tracker, trackerKey)
|
||||
local key = trackerKey or tostring(tonumber(tracker and tracker.id) or 0)
|
||||
if tracker and tracker.testMode then
|
||||
return self:CollectTrackerTestEntries(tracker), true
|
||||
end
|
||||
if tracker and tracker.demoMode then
|
||||
return self:GetDemoEntries(key, self:GetTrackerSpellPool(tracker.categories), tracker), true
|
||||
end
|
||||
if not tracker or not tracker.enabled or not self:IsVisibleForCurrentGroup(tracker) then
|
||||
return {}, false
|
||||
end
|
||||
return self:CollectTrackerEntries(tracker), true
|
||||
end
|
||||
|
||||
function HMGT:BuildEntriesByPlayerForTracker(tracker, trackerKey, resolveUnitAnchorFrame)
|
||||
local key = trackerKey or tostring(tonumber(tracker and tracker.id) or 0)
|
||||
local ownName = self:NormalizePlayerName(UnitName("player")) or "Player"
|
||||
if tracker.testMode then
|
||||
local entries = self:CollectTrackerTestEntries(tracker)
|
||||
if self:IsGroupTrackerConfig(tracker) and tracker.attachToPartyFrame == true then
|
||||
return self:BuildPartyPreviewEntries(entries, resolveUnitAnchorFrame)
|
||||
end
|
||||
local byPlayer, order, unitByPlayer = {}, {}, {}
|
||||
if #entries > 0 then
|
||||
byPlayer[ownName] = entries
|
||||
order[1] = ownName
|
||||
unitByPlayer[ownName] = "player"
|
||||
end
|
||||
return byPlayer, order, unitByPlayer, true
|
||||
end
|
||||
if tracker.demoMode then
|
||||
local entries = self:GetDemoEntries(key, self:GetTrackerSpellPool(tracker.categories), tracker)
|
||||
if self:IsGroupTrackerConfig(tracker) and tracker.attachToPartyFrame == true then
|
||||
return self:BuildPartyPreviewEntries(entries, resolveUnitAnchorFrame)
|
||||
end
|
||||
for _, entry in ipairs(entries) do
|
||||
entry.playerName = ownName
|
||||
end
|
||||
local byPlayer, order, unitByPlayer = {}, {}, {}
|
||||
if #entries > 0 then
|
||||
byPlayer[ownName] = entries
|
||||
order[1] = ownName
|
||||
unitByPlayer[ownName] = "player"
|
||||
end
|
||||
return byPlayer, order, unitByPlayer, true
|
||||
end
|
||||
if not tracker.enabled or not self:IsVisibleForCurrentGroup(tracker) then
|
||||
return {}, {}, {}, false
|
||||
end
|
||||
if IsInRaid() or not IsInGroup() then
|
||||
return {}, {}, {}, false
|
||||
end
|
||||
local byPlayer, order, unitByPlayer = {}, {}, {}
|
||||
for _, playerInfo in ipairs(self:GetTrackerPlayers(tracker)) do
|
||||
local entries = self:CollectTrackerEntriesForPlayer(tracker, playerInfo)
|
||||
if #entries > 0 then
|
||||
local playerName = playerInfo.name
|
||||
byPlayer[playerName] = entries
|
||||
order[#order + 1] = playerName
|
||||
unitByPlayer[playerName] = playerInfo.unitId
|
||||
end
|
||||
end
|
||||
return byPlayer, order, unitByPlayer, true
|
||||
end
|
||||
|
||||
function HMGT:FinalizeTrackerEntries(tracker, entries, trackerKey)
|
||||
local result = entries or {}
|
||||
if self.FilterDisplayEntries then
|
||||
result = self:FilterDisplayEntries(tracker, result) or result
|
||||
end
|
||||
if self.SortDisplayEntries then
|
||||
self:SortDisplayEntries(result, trackerKey)
|
||||
end
|
||||
|
||||
local shouldTick = false
|
||||
for _, entry in ipairs(result) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return result, shouldTick
|
||||
end
|
||||
|
||||
function HMGT:TriggerTrackerUpdate(reason)
|
||||
local function normalizeReason(value)
|
||||
if value == true then
|
||||
return "trackers"
|
||||
elseif value == "trackers" or value == "layout" or value == "visual" then
|
||||
return value
|
||||
end
|
||||
return "full"
|
||||
end
|
||||
|
||||
local function mergeReasons(current, incoming)
|
||||
local priority = {
|
||||
visual = 1,
|
||||
layout = 2,
|
||||
trackers = 3,
|
||||
full = 4,
|
||||
}
|
||||
current = normalizeReason(current)
|
||||
incoming = normalizeReason(incoming)
|
||||
if (priority[incoming] or 4) >= (priority[current] or 4) then
|
||||
return incoming
|
||||
end
|
||||
return current
|
||||
end
|
||||
|
||||
self._trackerUpdateMinDelay = self._trackerUpdateMinDelay or 0.08
|
||||
self._trackerUpdatePending = true
|
||||
self._trackerUpdateReason = mergeReasons(self._trackerUpdateReason, reason)
|
||||
if HMGT.TrackerManager then
|
||||
local normalizedReason = normalizeReason(reason)
|
||||
if normalizedReason == "trackers" then
|
||||
HMGT.TrackerManager:MarkTrackersDirty()
|
||||
elseif normalizedReason == "layout" then
|
||||
HMGT.TrackerManager:MarkLayoutDirty()
|
||||
end
|
||||
end
|
||||
if self._updateScheduled then return end
|
||||
|
||||
local now = GetTime()
|
||||
local last = self._lastTrackerUpdateAt or 0
|
||||
local delay = math.max(0, self._trackerUpdateMinDelay - (now - last))
|
||||
self._updateScheduled = true
|
||||
|
||||
self:ScheduleTimer(function()
|
||||
self._updateScheduled = nil
|
||||
if not self._trackerUpdatePending then return end
|
||||
self._trackerUpdatePending = nil
|
||||
self._lastTrackerUpdateAt = GetTime()
|
||||
local pendingReason = self._trackerUpdateReason
|
||||
self._trackerUpdateReason = nil
|
||||
|
||||
local function profileModule(name, fn)
|
||||
if not fn then return end
|
||||
local t0 = debugprofilestop and debugprofilestop() or nil
|
||||
fn()
|
||||
local t1 = debugprofilestop and debugprofilestop() or nil
|
||||
if t0 and t1 then
|
||||
local mod = HMGT[name]
|
||||
local count = mod and mod.lastEntryCount or 0
|
||||
self:DebugScoped("verbose", "TrackerUI", "UIUpdate %s took %.2fms entries=%s", tostring(name), t1 - t0, tostring(count))
|
||||
end
|
||||
end
|
||||
|
||||
profileModule("TrackerManager", HMGT.TrackerManager and function()
|
||||
if pendingReason == "visual" and HMGT.TrackerManager.RefreshVisibleVisuals then
|
||||
HMGT.TrackerManager:RefreshVisibleVisuals()
|
||||
else
|
||||
HMGT.TrackerManager:UpdateDisplay()
|
||||
end
|
||||
end or nil)
|
||||
|
||||
if self._trackerUpdatePending then
|
||||
self:TriggerTrackerUpdate()
|
||||
end
|
||||
end, delay)
|
||||
end
|
||||
268
Modules/Tracker/TrackerDataProvider.lua
Normal file
268
Modules/Tracker/TrackerDataProvider.lua
Normal file
@@ -0,0 +1,268 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
HMGT.TrackerDataProvider = HMGT.TrackerDataProvider or {}
|
||||
|
||||
local internals = HMGT.TrackerInternals or {}
|
||||
local SafeApiNumber = internals.SafeApiNumber
|
||||
local GetSpellChargesInfo = internals.GetSpellChargesInfo
|
||||
local GetSpellCooldownInfo = internals.GetSpellCooldownInfo
|
||||
|
||||
function HMGT:GetCooldownInfo(playerName, spellId, opts)
|
||||
opts = opts or {}
|
||||
local deferUntilEmpty = opts.deferChargeCooldownUntilEmpty and true or false
|
||||
local spellEntry = HMGT_SpellData.InterruptLookup[spellId]
|
||||
or HMGT_SpellData.CooldownLookup[spellId]
|
||||
local ownName = self:NormalizePlayerName(UnitName("player"))
|
||||
local isOwnPlayer = playerName == ownName
|
||||
local pData = isOwnPlayer and self.playerData[ownName] or nil
|
||||
local talents = pData and pData.talents or {}
|
||||
local effectiveCd = spellEntry and HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents) or 0
|
||||
local knownMaxCharges, knownChargeDuration = 0, 0
|
||||
if spellEntry and isOwnPlayer then
|
||||
knownMaxCharges, knownChargeDuration = self:GetKnownChargeInfo(spellEntry, talents, spellId, effectiveCd)
|
||||
end
|
||||
|
||||
if self:IsAvailabilitySpell(spellEntry) then
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
if normalizedName == ownName then
|
||||
local current, max = self:GetOwnAvailabilityProgress(spellEntry)
|
||||
if (tonumber(max) or 0) > 0 then
|
||||
self:StoreAvailabilityState(ownName, spellId, current, max, spellEntry)
|
||||
return 0, 0, current, max
|
||||
end
|
||||
else
|
||||
local current, max = self:GetAvailabilityState(normalizedName, spellId)
|
||||
if (tonumber(max) or 0) > 0 then
|
||||
return 0, 0, current, max
|
||||
end
|
||||
end
|
||||
return 0, 0, nil, nil
|
||||
end
|
||||
|
||||
local cdData = self:GetActiveCooldown(playerName, spellId)
|
||||
|
||||
if isOwnPlayer and not (InCombatLockdown and InCombatLockdown()) then
|
||||
local charges, maxCharges, chargeStart, chargeDuration = nil, nil, nil, nil
|
||||
if GetSpellChargesInfo then
|
||||
charges, maxCharges, chargeStart, chargeDuration = GetSpellChargesInfo(spellId)
|
||||
end
|
||||
charges = SafeApiNumber and SafeApiNumber(charges, 0) or tonumber(charges) or 0
|
||||
maxCharges = SafeApiNumber and SafeApiNumber(maxCharges, 0) or tonumber(maxCharges) or 0
|
||||
chargeStart = SafeApiNumber and SafeApiNumber(chargeStart) or tonumber(chargeStart)
|
||||
chargeDuration = SafeApiNumber and SafeApiNumber(chargeDuration, 0) or tonumber(chargeDuration) or 0
|
||||
|
||||
if maxCharges > 0 then
|
||||
local tempChargeState = {
|
||||
currentCharges = charges,
|
||||
maxCharges = maxCharges,
|
||||
chargeStart = chargeStart,
|
||||
chargeDuration = chargeDuration,
|
||||
duration = chargeDuration,
|
||||
}
|
||||
local remaining, total, curCharges, maxChargeCount = self:ResolveChargeState(tempChargeState)
|
||||
self:StoreKnownChargeInfo(spellId, maxChargeCount, total > 0 and total or chargeDuration)
|
||||
if (curCharges or 0) < maxChargeCount and remaining <= 0 and GetSpellCooldownInfo then
|
||||
local cdStart, cdDuration = GetSpellCooldownInfo(spellId)
|
||||
if cdDuration > 0 then
|
||||
remaining = math.max(0, cdDuration - (GetTime() - cdStart))
|
||||
total = math.max(total or 0, cdDuration)
|
||||
end
|
||||
end
|
||||
if deferUntilEmpty and (curCharges or 0) > 0 then
|
||||
remaining = 0
|
||||
end
|
||||
return remaining, total, curCharges, maxChargeCount
|
||||
end
|
||||
|
||||
if GetSpellCooldownInfo then
|
||||
local cdStart, cdDuration = GetSpellCooldownInfo(spellId)
|
||||
cdStart = tonumber(cdStart) or 0
|
||||
cdDuration = tonumber(cdDuration) or 0
|
||||
if cdDuration > 0 then
|
||||
local remaining = math.max(0, cdDuration - (GetTime() - cdStart))
|
||||
remaining = math.max(0, math.min(cdDuration, remaining))
|
||||
if cdData and (tonumber(cdData.maxCharges) or 0) <= 0 then
|
||||
local cachedRemaining = (tonumber(cdData.duration) or 0) - (GetTime() - (tonumber(cdData.startTime) or GetTime()))
|
||||
cachedRemaining = math.max(0, math.min(tonumber(cdData.duration) or cachedRemaining, cachedRemaining))
|
||||
local cachedDuration = math.max(0, tonumber(cdData.duration) or 0)
|
||||
if cachedDuration > 2.0 and cachedRemaining > 2.0 and cdDuration < math.max(2.0, cachedDuration * 0.35) then
|
||||
return cachedRemaining, cachedDuration, nil, nil
|
||||
end
|
||||
end
|
||||
return remaining, cdDuration, nil, nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not cdData then
|
||||
if isOwnPlayer and knownMaxCharges > 1 then
|
||||
return 0, math.max(0, knownChargeDuration or effectiveCd or 0), knownMaxCharges, knownMaxCharges
|
||||
end
|
||||
return 0, 0, nil, nil
|
||||
end
|
||||
if (tonumber(cdData.maxCharges) or 0) > 0 then
|
||||
local remaining, chargeDur, charges, maxCharges = self:ResolveChargeState(cdData)
|
||||
self:StoreKnownChargeInfo(spellId, maxCharges, chargeDur)
|
||||
if deferUntilEmpty and charges > 0 then
|
||||
remaining = 0
|
||||
end
|
||||
return remaining, chargeDur, charges, maxCharges
|
||||
end
|
||||
if isOwnPlayer and knownMaxCharges > 1 then
|
||||
local remaining = (tonumber(cdData.duration) or 0) - (GetTime() - (tonumber(cdData.startTime) or GetTime()))
|
||||
remaining = math.max(0, math.min(tonumber(cdData.duration) or remaining, remaining))
|
||||
local currentCharges = knownMaxCharges
|
||||
if remaining > 0 then
|
||||
currentCharges = math.max(0, knownMaxCharges - 1)
|
||||
end
|
||||
if deferUntilEmpty and currentCharges > 0 then
|
||||
remaining = 0
|
||||
end
|
||||
return remaining, math.max(0, knownChargeDuration or effectiveCd or 0), currentCharges, knownMaxCharges
|
||||
end
|
||||
local remaining = cdData.duration - (GetTime() - cdData.startTime)
|
||||
remaining = math.max(0, math.min(cdData.duration, remaining))
|
||||
return remaining, cdData.duration, nil, nil
|
||||
end
|
||||
|
||||
function HMGT:ShouldDisplayEntry(settings, remaining, currentCharges, maxCharges, spellEntry)
|
||||
local rem = tonumber(remaining) or 0
|
||||
local cur = tonumber(currentCharges) or 0
|
||||
local max = tonumber(maxCharges) or 0
|
||||
local soon = tonumber(settings.readySoonSec) or 0
|
||||
local isAvailabilitySpell = spellEntry and self:IsAvailabilitySpell(spellEntry) or false
|
||||
local isReady
|
||||
|
||||
if isAvailabilitySpell then
|
||||
isReady = max > 0 and cur >= max
|
||||
else
|
||||
isReady = rem <= 0 or (max > 0 and cur > 0)
|
||||
end
|
||||
|
||||
if settings.showOnlyReady then
|
||||
return isReady
|
||||
end
|
||||
if soon > 0 then
|
||||
if isAvailabilitySpell then
|
||||
return isReady
|
||||
end
|
||||
return isReady or rem <= soon
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local DEFAULT_CATEGORY_PRIORITY = {
|
||||
interrupt = 1,
|
||||
lust = 2,
|
||||
defensive = 3,
|
||||
tank = 4,
|
||||
healing = 5,
|
||||
offensive = 6,
|
||||
utility = 7,
|
||||
cc = 8,
|
||||
}
|
||||
|
||||
local TRACKER_CATEGORY_PRIORITY = {
|
||||
interruptTracker = {
|
||||
interrupt = 1,
|
||||
defensive = 2,
|
||||
utility = 3,
|
||||
cc = 4,
|
||||
healing = 5,
|
||||
tank = 6,
|
||||
offensive = 7,
|
||||
lust = 8,
|
||||
},
|
||||
raidCooldownTracker = {
|
||||
lust = 1,
|
||||
defensive = 2,
|
||||
healing = 3,
|
||||
tank = 4,
|
||||
utility = 5,
|
||||
offensive = 6,
|
||||
cc = 7,
|
||||
interrupt = 8,
|
||||
},
|
||||
groupCooldownTracker = {
|
||||
tank = 1,
|
||||
defensive = 2,
|
||||
healing = 3,
|
||||
cc = 4,
|
||||
utility = 5,
|
||||
offensive = 6,
|
||||
lust = 7,
|
||||
interrupt = 8,
|
||||
},
|
||||
}
|
||||
|
||||
local function GetCategoryPriority(category, trackerKey)
|
||||
local cat = tostring(category or "utility")
|
||||
local trackerOrder = trackerKey and TRACKER_CATEGORY_PRIORITY[trackerKey]
|
||||
if trackerOrder and trackerOrder[cat] then
|
||||
return trackerOrder[cat]
|
||||
end
|
||||
local order = HMGT_SpellData and HMGT_SpellData.CategoryOrder
|
||||
if type(order) == "table" then
|
||||
for idx, key in ipairs(order) do
|
||||
if key == cat then
|
||||
return idx
|
||||
end
|
||||
end
|
||||
return #order + 10
|
||||
end
|
||||
return DEFAULT_CATEGORY_PRIORITY[cat] or 99
|
||||
end
|
||||
|
||||
function HMGT:SortDisplayEntries(entries, trackerKey)
|
||||
if type(entries) ~= "table" then return end
|
||||
table.sort(entries, function(a, b)
|
||||
local aRemaining = tonumber(a and a.remaining) or 0
|
||||
local bRemaining = tonumber(b and b.remaining) or 0
|
||||
local aActive = aRemaining > 0
|
||||
local bActive = bRemaining > 0
|
||||
if aActive ~= bActive then
|
||||
return aActive
|
||||
end
|
||||
|
||||
local aEntry = a and a.spellEntry
|
||||
local bEntry = b and b.spellEntry
|
||||
|
||||
local aPriority = tonumber(aEntry and aEntry.priority) or GetCategoryPriority(aEntry and aEntry.category, trackerKey)
|
||||
local bPriority = tonumber(bEntry and bEntry.priority) or GetCategoryPriority(bEntry and bEntry.category, trackerKey)
|
||||
if aPriority ~= bPriority then
|
||||
return aPriority < bPriority
|
||||
end
|
||||
|
||||
if aActive and aRemaining ~= bRemaining then
|
||||
return aRemaining < bRemaining
|
||||
end
|
||||
|
||||
local aTotal = tonumber(a and a.total)
|
||||
or tonumber(aEntry and HMGT_SpellData.GetBaseCooldown and HMGT_SpellData.GetBaseCooldown(aEntry))
|
||||
or tonumber(aEntry and aEntry.cooldown)
|
||||
or 0
|
||||
local bTotal = tonumber(b and b.total)
|
||||
or tonumber(bEntry and HMGT_SpellData.GetBaseCooldown and HMGT_SpellData.GetBaseCooldown(bEntry))
|
||||
or tonumber(bEntry and bEntry.cooldown)
|
||||
or 0
|
||||
if (not aActive) and aTotal ~= bTotal then
|
||||
return aTotal > bTotal
|
||||
end
|
||||
|
||||
if aRemaining ~= bRemaining then
|
||||
return aRemaining < bRemaining
|
||||
end
|
||||
|
||||
local aName = tostring(a and a.playerName or "")
|
||||
local bName = tostring(b and b.playerName or "")
|
||||
if aName ~= bName then
|
||||
return aName < bName
|
||||
end
|
||||
|
||||
local aSpell = tonumber(aEntry and aEntry.spellId) or 0
|
||||
local bSpell = tonumber(bEntry and bEntry.spellId) or 0
|
||||
return aSpell < bSpell
|
||||
end)
|
||||
end
|
||||
524
Modules/Tracker/TrackerDetection.lua
Normal file
524
Modules/Tracker/TrackerDetection.lua
Normal file
@@ -0,0 +1,524 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
HMGT.TrackerDetection = HMGT.TrackerDetection or {}
|
||||
|
||||
local internals = HMGT.TrackerInternals or {}
|
||||
local GetSpellChargesInfo = internals.GetSpellChargesInfo
|
||||
local GetSpellCooldownInfo = internals.GetSpellCooldownInfo
|
||||
local GetGlobalCooldownInfo = internals.GetGlobalCooldownInfo
|
||||
local GetSpellDebugLabel = internals.GetSpellDebugLabel
|
||||
local BuildCooldownStateFingerprint = internals.BuildCooldownStateFingerprint
|
||||
local ApplyOwnCooldownReducers = internals.ApplyOwnCooldownReducers
|
||||
local ApplyObservedCooldownReducers = internals.ApplyObservedCooldownReducers
|
||||
|
||||
function HMGT:HandleOwnSpellCast(spellId)
|
||||
local isInterrupt = HMGT_SpellData.InterruptLookup[spellId] ~= nil
|
||||
local isCooldown = HMGT_SpellData.CooldownLookup[spellId] ~= nil
|
||||
if not isInterrupt and not isCooldown then return end
|
||||
|
||||
local spellEntry = HMGT_SpellData.InterruptLookup[spellId]
|
||||
or HMGT_SpellData.CooldownLookup[spellId]
|
||||
spellId = tonumber(spellEntry and spellEntry.spellId) or spellId
|
||||
local name = self:NormalizePlayerName(UnitName("player"))
|
||||
local pData = self.playerData[name]
|
||||
local talents = pData and pData.talents or {}
|
||||
if self:IsAvailabilitySpell(spellEntry) then
|
||||
self:LogTrackedSpellCast(name, spellEntry, {
|
||||
stateKind = "availability",
|
||||
required = HMGT_SpellData.GetEffectiveAvailabilityRequired(spellEntry, talents),
|
||||
})
|
||||
if self:RefreshOwnAvailabilitySpell(spellEntry) then
|
||||
self:PublishOwnSpellState(spellId, { sendLegacy = true })
|
||||
end
|
||||
self:TriggerTrackerUpdate()
|
||||
return
|
||||
end
|
||||
|
||||
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
local now = GetTime()
|
||||
local inCombat = InCombatLockdown and InCombatLockdown()
|
||||
local cur, max, chargeStart, chargeDuration = nil, nil, nil, nil
|
||||
if not inCombat and GetSpellChargesInfo then
|
||||
cur, max, chargeStart, chargeDuration = GetSpellChargesInfo(spellId)
|
||||
end
|
||||
local cachedMaxCharges, cachedChargeDuration = self:GetKnownChargeInfo(
|
||||
spellEntry,
|
||||
talents,
|
||||
spellId,
|
||||
(not inCombat and tonumber(chargeDuration) and tonumber(chargeDuration) > 0) and tonumber(chargeDuration) or effectiveCd
|
||||
)
|
||||
local inferredMaxCharges, inferredChargeDuration = HMGT_SpellData.GetEffectiveChargeInfo(
|
||||
spellEntry,
|
||||
talents,
|
||||
(not inCombat and tonumber(max) and tonumber(max) > 0) and tonumber(max) or ((cachedMaxCharges > 0) and cachedMaxCharges or nil),
|
||||
(not inCombat and tonumber(chargeDuration) and tonumber(chargeDuration) > 0) and tonumber(chargeDuration)
|
||||
or ((cachedChargeDuration > 0) and cachedChargeDuration or effectiveCd)
|
||||
)
|
||||
|
||||
local hasCharges = ((tonumber(max) or 0) > 1) or (tonumber(inferredMaxCharges) or 0) > 1
|
||||
local currentCharges = 0
|
||||
local maxCharges = 0
|
||||
local chargeDur = 0
|
||||
local chargeStartTime = nil
|
||||
|
||||
local startTime = now
|
||||
local duration = effectiveCd
|
||||
local expiresIn = effectiveCd
|
||||
|
||||
local existingCd = self:GetActiveCooldown(name, spellId)
|
||||
if existingCd and (tonumber(existingCd.maxCharges) or 0) > 0 then
|
||||
self:ResolveChargeState(existingCd, now)
|
||||
end
|
||||
|
||||
if hasCharges then
|
||||
maxCharges = math.max(1, tonumber(max) or cachedMaxCharges or tonumber(inferredMaxCharges) or 1)
|
||||
currentCharges = tonumber(cur)
|
||||
if currentCharges == nil then
|
||||
local prevCharges = existingCd and tonumber(existingCd.currentCharges)
|
||||
local prevMax = existingCd and tonumber(existingCd.maxCharges)
|
||||
if prevCharges and prevMax and prevMax == maxCharges then
|
||||
currentCharges = math.max(0, prevCharges - 1)
|
||||
else
|
||||
currentCharges = math.max(0, maxCharges - 1)
|
||||
end
|
||||
end
|
||||
currentCharges = math.max(0, math.min(maxCharges, currentCharges))
|
||||
|
||||
chargeDur = tonumber(chargeDuration)
|
||||
or cachedChargeDuration
|
||||
or tonumber(inferredChargeDuration)
|
||||
or tonumber(effectiveCd)
|
||||
or 0
|
||||
chargeDur = math.max(0, chargeDur)
|
||||
self:StoreKnownChargeInfo(spellId, maxCharges, chargeDur)
|
||||
|
||||
if currentCharges < maxCharges and chargeDur > 0 then
|
||||
chargeStartTime = tonumber(chargeStart) or now
|
||||
local missing = maxCharges - currentCharges
|
||||
startTime = chargeStartTime
|
||||
duration = missing * chargeDur
|
||||
expiresIn = math.max(0, duration - (now - startTime))
|
||||
else
|
||||
startTime = now
|
||||
duration = 0
|
||||
expiresIn = 0
|
||||
end
|
||||
end
|
||||
|
||||
self:Debug(
|
||||
"verbose",
|
||||
"HandleOwnSpellCast name=%s spellId=%s cd=%.2f charges=%s/%s",
|
||||
tostring(name),
|
||||
tostring(spellId),
|
||||
tonumber(effectiveCd) or 0,
|
||||
hasCharges and tostring(currentCharges) or "-",
|
||||
hasCharges and tostring(maxCharges) or "-"
|
||||
)
|
||||
|
||||
self._cdNonce = (self._cdNonce or 0) + 1
|
||||
local nonce = self._cdNonce
|
||||
|
||||
self:SetActiveCooldown(name, spellId, {
|
||||
startTime = startTime,
|
||||
duration = duration,
|
||||
spellEntry = spellEntry,
|
||||
currentCharges = hasCharges and currentCharges or nil,
|
||||
maxCharges = hasCharges and maxCharges or nil,
|
||||
chargeStart = hasCharges and chargeStartTime or nil,
|
||||
chargeDuration = hasCharges and chargeDur or nil,
|
||||
_nonce = nonce,
|
||||
})
|
||||
|
||||
self:LogTrackedSpellCast(name, spellEntry, {
|
||||
cooldown = effectiveCd,
|
||||
currentCharges = hasCharges and currentCharges or nil,
|
||||
maxCharges = hasCharges and maxCharges or nil,
|
||||
chargeCooldown = hasCharges and chargeDur or nil,
|
||||
})
|
||||
|
||||
if expiresIn > 0 then
|
||||
self:ScheduleTimer(function()
|
||||
local current = self:GetActiveCooldown(name, spellId)
|
||||
if current and current._nonce == nonce then
|
||||
self:ClearActiveCooldown(name, spellId)
|
||||
self:PublishOwnSpellState(spellId)
|
||||
self:TriggerTrackerUpdate()
|
||||
end
|
||||
end, expiresIn)
|
||||
end
|
||||
|
||||
self:PublishOwnSpellState(spellId, { sendLegacy = true })
|
||||
self:TriggerTrackerUpdate()
|
||||
end
|
||||
|
||||
function HMGT:RefreshOwnCooldownStateFromGame(spellId)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid then return false end
|
||||
if InCombatLockdown and InCombatLockdown() then
|
||||
return false
|
||||
end
|
||||
|
||||
local ownName = self:NormalizePlayerName(UnitName("player"))
|
||||
if not ownName then return false end
|
||||
|
||||
local spellEntry = HMGT_SpellData.InterruptLookup[sid]
|
||||
or HMGT_SpellData.CooldownLookup[sid]
|
||||
if not spellEntry or self:IsAvailabilitySpell(spellEntry) then
|
||||
return false
|
||||
end
|
||||
sid = tonumber(spellEntry.spellId) or sid
|
||||
|
||||
local existing = self:GetActiveCooldown(ownName, sid)
|
||||
local before = BuildCooldownStateFingerprint and BuildCooldownStateFingerprint(existing) or "nil"
|
||||
local now = GetTime()
|
||||
local pData = self.playerData[ownName]
|
||||
local talents = pData and pData.talents or {}
|
||||
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
local cur, max, chargeStart, chargeDuration = nil, nil, nil, nil
|
||||
if GetSpellChargesInfo then
|
||||
cur, max, chargeStart, chargeDuration = GetSpellChargesInfo(sid)
|
||||
end
|
||||
local inferredMaxCharges, inferredChargeDuration = HMGT_SpellData.GetEffectiveChargeInfo(
|
||||
spellEntry,
|
||||
talents,
|
||||
(tonumber(max) and tonumber(max) > 0) and tonumber(max) or nil,
|
||||
(tonumber(chargeDuration) and tonumber(chargeDuration) > 0) and tonumber(chargeDuration) or effectiveCd
|
||||
)
|
||||
|
||||
local hasCharges = ((tonumber(max) or 0) > 1) or (tonumber(inferredMaxCharges) or 0) > 1
|
||||
|
||||
if hasCharges then
|
||||
local maxCharges = math.max(1, tonumber(max) or tonumber(inferredMaxCharges) or 1)
|
||||
local currentCharges = tonumber(cur)
|
||||
if currentCharges == nil then
|
||||
currentCharges = maxCharges
|
||||
end
|
||||
currentCharges = math.max(0, math.min(maxCharges, currentCharges))
|
||||
|
||||
local chargeDur = tonumber(chargeDuration) or tonumber(inferredChargeDuration) or tonumber(effectiveCd) or 0
|
||||
chargeDur = math.max(0, chargeDur)
|
||||
|
||||
if currentCharges < maxCharges and chargeDur > 0 then
|
||||
local chargeStartTime = tonumber(chargeStart) or now
|
||||
local missing = maxCharges - currentCharges
|
||||
local updatedEntry = self:SetActiveCooldown(ownName, sid, {
|
||||
startTime = chargeStartTime,
|
||||
duration = missing * chargeDur,
|
||||
spellEntry = spellEntry,
|
||||
currentCharges = currentCharges,
|
||||
maxCharges = maxCharges,
|
||||
chargeStart = chargeStartTime,
|
||||
chargeDuration = chargeDur,
|
||||
})
|
||||
self:RefreshCooldownExpiryTimer(ownName, sid, updatedEntry)
|
||||
else
|
||||
self:ClearActiveCooldown(ownName, sid)
|
||||
end
|
||||
else
|
||||
local cooldownStart, cooldownDuration = 0, 0
|
||||
if GetSpellCooldownInfo then
|
||||
cooldownStart, cooldownDuration = GetSpellCooldownInfo(sid)
|
||||
end
|
||||
cooldownStart = tonumber(cooldownStart) or 0
|
||||
cooldownDuration = tonumber(cooldownDuration) or 0
|
||||
local gcdStart, gcdDuration = 0, 0
|
||||
if GetGlobalCooldownInfo then
|
||||
gcdStart, gcdDuration = GetGlobalCooldownInfo()
|
||||
end
|
||||
gcdStart = tonumber(gcdStart) or 0
|
||||
gcdDuration = tonumber(gcdDuration) or 0
|
||||
local existingDuration = tonumber(existing and existing.duration) or 0
|
||||
local existingStart = tonumber(existing and existing.startTime) or now
|
||||
local existingRemaining = math.max(0, existingDuration - (now - existingStart))
|
||||
|
||||
local isLikelyGlobalCooldown = cooldownDuration > 0
|
||||
and gcdDuration > 0
|
||||
and math.abs(cooldownDuration - gcdDuration) <= 0.15
|
||||
and (tonumber(effectiveCd) or 0) > (gcdDuration + 1.0)
|
||||
|
||||
local isSuspiciousShortRefresh = cooldownDuration > 0
|
||||
and existingRemaining > 2.0
|
||||
and existingDuration > 2.0
|
||||
and cooldownDuration < math.max(2.0, existingDuration * 0.35)
|
||||
and cooldownDuration < math.max(2.0, (tonumber(effectiveCd) or 0) * 0.35)
|
||||
|
||||
if isLikelyGlobalCooldown or isSuspiciousShortRefresh then
|
||||
self:DebugScoped(
|
||||
"verbose",
|
||||
"TrackerState",
|
||||
"Ignore suspicious refresh for %s: spellCD=%.3f gcd=%.3f existing=%.3f remaining=%.3f effective=%.3f",
|
||||
GetSpellDebugLabel and GetSpellDebugLabel(sid) or tostring(sid),
|
||||
cooldownDuration,
|
||||
gcdDuration,
|
||||
existingDuration,
|
||||
existingRemaining,
|
||||
tonumber(effectiveCd) or 0
|
||||
)
|
||||
return false
|
||||
end
|
||||
|
||||
if cooldownDuration > 0 then
|
||||
local updatedEntry = self:SetActiveCooldown(ownName, sid, {
|
||||
startTime = cooldownStart,
|
||||
duration = cooldownDuration,
|
||||
spellEntry = spellEntry,
|
||||
})
|
||||
self:RefreshCooldownExpiryTimer(ownName, sid, updatedEntry)
|
||||
else
|
||||
self:ClearActiveCooldown(ownName, sid)
|
||||
end
|
||||
end
|
||||
|
||||
local after = BuildCooldownStateFingerprint and BuildCooldownStateFingerprint(self:GetActiveCooldown(ownName, sid)) or "nil"
|
||||
return before ~= after
|
||||
end
|
||||
|
||||
function HMGT:DidOwnInterruptSucceed(triggerSpellId, talents)
|
||||
local sid = tonumber(triggerSpellId)
|
||||
if not sid then return false end
|
||||
|
||||
local spellEntry = HMGT_SpellData and HMGT_SpellData.InterruptLookup and HMGT_SpellData.InterruptLookup[sid]
|
||||
if not spellEntry then return false end
|
||||
sid = tonumber(spellEntry.spellId) or sid
|
||||
|
||||
local observedDuration = 0
|
||||
if GetSpellCooldownInfo then
|
||||
local _, duration = GetSpellCooldownInfo(sid)
|
||||
observedDuration = duration
|
||||
end
|
||||
observedDuration = tonumber(observedDuration) or 0
|
||||
if observedDuration <= 0 then return false end
|
||||
|
||||
local expectedDuration = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
expectedDuration = tonumber(expectedDuration) or 0
|
||||
if expectedDuration <= 0 then return false end
|
||||
|
||||
return observedDuration < (expectedDuration - 0.05)
|
||||
end
|
||||
|
||||
function HMGT:HandleOwnCooldownReductionTrigger(triggerSpellId)
|
||||
local ownName = self:NormalizePlayerName(UnitName("player"))
|
||||
if not ownName then return end
|
||||
|
||||
local pData = self.playerData[ownName]
|
||||
local classToken = pData and pData.class or select(2, UnitClass("player"))
|
||||
local specIndex = pData and pData.specIndex or GetSpecialization()
|
||||
local talents = pData and pData.talents or {}
|
||||
if not classToken or not specIndex then return end
|
||||
|
||||
local reducers = HMGT_SpellData.GetCooldownReducersForCast(classToken, specIndex, triggerSpellId, talents)
|
||||
if not reducers or #reducers == 0 then return end
|
||||
|
||||
local instantReducers = {}
|
||||
local observedInstantReducers = {}
|
||||
local successReducers = {}
|
||||
local observedSuccessReducers = {}
|
||||
for _, reducer in ipairs(reducers) do
|
||||
local observed = type(reducer.observe) == "table"
|
||||
if reducer.requireInterruptSuccess then
|
||||
if observed then
|
||||
observedSuccessReducers[#observedSuccessReducers + 1] = reducer
|
||||
else
|
||||
successReducers[#successReducers + 1] = reducer
|
||||
end
|
||||
else
|
||||
if observed then
|
||||
observedInstantReducers[#observedInstantReducers + 1] = reducer
|
||||
else
|
||||
instantReducers[#instantReducers + 1] = reducer
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local castTs = GetServerTime()
|
||||
if #instantReducers > 0 and ApplyOwnCooldownReducers then
|
||||
ApplyOwnCooldownReducers(self, ownName, triggerSpellId, instantReducers, castTs)
|
||||
end
|
||||
if #observedInstantReducers > 0 and ApplyObservedCooldownReducers then
|
||||
ApplyObservedCooldownReducers(self, ownName, observedInstantReducers)
|
||||
end
|
||||
|
||||
if #successReducers > 0 or #observedSuccessReducers > 0 then
|
||||
local function ApplySuccessReducers()
|
||||
if not self:DidOwnInterruptSucceed(triggerSpellId, talents) then
|
||||
return false
|
||||
end
|
||||
if #successReducers > 0 and ApplyOwnCooldownReducers then
|
||||
ApplyOwnCooldownReducers(self, ownName, triggerSpellId, successReducers, castTs)
|
||||
end
|
||||
if #observedSuccessReducers > 0 and ApplyObservedCooldownReducers then
|
||||
ApplyObservedCooldownReducers(self, ownName, observedSuccessReducers)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
if not ApplySuccessReducers() then
|
||||
C_Timer.After(0.12, function()
|
||||
if not self or not self.playerData or not self.playerData[ownName] then
|
||||
return
|
||||
end
|
||||
ApplySuccessReducers()
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function HMGT:HandleRemoteSpellCast(playerName, spellId, castTimestamp, curCharges, maxCharges, chargeRemaining, chargeDuration)
|
||||
local spellEntry = HMGT_SpellData.InterruptLookup[spellId]
|
||||
or HMGT_SpellData.CooldownLookup[spellId]
|
||||
if not spellEntry then return end
|
||||
spellId = tonumber(spellEntry.spellId) or spellId
|
||||
if self:IsAvailabilitySpell(spellEntry) then return end
|
||||
|
||||
local pData = self.playerData[playerName]
|
||||
local talents = pData and pData.talents or {}
|
||||
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
|
||||
castTimestamp = tonumber(castTimestamp) or GetServerTime()
|
||||
local existingEntry = self:GetActiveCooldown(playerName, spellId)
|
||||
if (tonumber(maxCharges) or 0) <= 0 and existingEntry and existingEntry.lastCastTimestamp then
|
||||
local prevTs = tonumber(existingEntry.lastCastTimestamp) or 0
|
||||
if math.abs(prevTs - castTimestamp) <= 1 then
|
||||
return
|
||||
end
|
||||
end
|
||||
local now = GetTime()
|
||||
local elapsed = math.max(0, GetServerTime() - castTimestamp)
|
||||
|
||||
local incomingCur = tonumber(curCharges) or 0
|
||||
local incomingMax = tonumber(maxCharges) or 0
|
||||
local incomingChargeRemaining = tonumber(chargeRemaining) or 0
|
||||
local incomingChargeDuration = tonumber(chargeDuration) or 0
|
||||
|
||||
local inferredMaxCharges, inferredChargeDuration = HMGT_SpellData.GetEffectiveChargeInfo(
|
||||
spellEntry,
|
||||
talents,
|
||||
(incomingMax > 0) and incomingMax or nil,
|
||||
(incomingChargeDuration > 0) and incomingChargeDuration or effectiveCd
|
||||
)
|
||||
local hasCharges = (incomingMax > 1) or (tonumber(inferredMaxCharges) or 0) > 1
|
||||
|
||||
local currentCharges = 0
|
||||
local maxChargeCount = 0
|
||||
local chargeDur = 0
|
||||
local nextChargeRemaining = 0
|
||||
local chargeStartTime = nil
|
||||
local startTime, duration, expiresIn
|
||||
|
||||
if hasCharges then
|
||||
maxChargeCount = math.max(1, (incomingMax > 0 and incomingMax) or tonumber(inferredMaxCharges) or 1)
|
||||
chargeDur = tonumber(incomingChargeDuration) or tonumber(inferredChargeDuration) or tonumber(effectiveCd) or 0
|
||||
chargeDur = math.max(0, chargeDur)
|
||||
if chargeDur <= 0 then
|
||||
chargeDur = math.max(0, tonumber(effectiveCd) or 0)
|
||||
end
|
||||
|
||||
if incomingMax > 0 then
|
||||
currentCharges = math.max(0, math.min(maxChargeCount, incomingCur))
|
||||
nextChargeRemaining = math.max(0, math.min(chargeDur, incomingChargeRemaining - elapsed))
|
||||
if currentCharges < maxChargeCount and chargeDur > 0 then
|
||||
chargeStartTime = now - math.max(0, chargeDur - nextChargeRemaining)
|
||||
end
|
||||
else
|
||||
local existing = self:GetActiveCooldown(playerName, spellId)
|
||||
if existing and (tonumber(existing.maxCharges) or 0) == maxChargeCount then
|
||||
self:ResolveChargeState(existing, now)
|
||||
local prevCharges = tonumber(existing.currentCharges) or maxChargeCount
|
||||
local prevStart = tonumber(existing.chargeStart)
|
||||
local prevDur = tonumber(existing.chargeDuration) or chargeDur
|
||||
if prevDur > 0 then
|
||||
chargeDur = prevDur
|
||||
end
|
||||
|
||||
currentCharges = math.max(0, prevCharges - 1)
|
||||
if currentCharges < maxChargeCount and chargeDur > 0 then
|
||||
if prevCharges >= maxChargeCount then
|
||||
chargeStartTime = now
|
||||
else
|
||||
chargeStartTime = prevStart or now
|
||||
end
|
||||
nextChargeRemaining = math.max(0, chargeDur - (now - chargeStartTime))
|
||||
end
|
||||
else
|
||||
currentCharges = math.max(0, maxChargeCount - 1)
|
||||
if currentCharges < maxChargeCount and chargeDur > 0 then
|
||||
chargeStartTime = now
|
||||
nextChargeRemaining = chargeDur
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if currentCharges >= maxChargeCount and maxChargeCount > 0 then
|
||||
currentCharges = math.max(0, maxChargeCount - 1)
|
||||
if chargeDur > 0 then
|
||||
chargeStartTime = now
|
||||
nextChargeRemaining = chargeDur
|
||||
end
|
||||
end
|
||||
|
||||
if currentCharges < maxChargeCount and chargeDur > 0 then
|
||||
chargeStartTime = chargeStartTime or now
|
||||
local missing = maxChargeCount - currentCharges
|
||||
startTime = chargeStartTime
|
||||
duration = missing * chargeDur
|
||||
expiresIn = math.max(0, duration - (now - startTime))
|
||||
else
|
||||
startTime = now
|
||||
duration = 0
|
||||
expiresIn = 0
|
||||
end
|
||||
else
|
||||
local remaining = effectiveCd - elapsed
|
||||
if remaining <= 0 then return end
|
||||
startTime = now - elapsed
|
||||
duration = effectiveCd
|
||||
expiresIn = remaining
|
||||
end
|
||||
|
||||
self:Debug(
|
||||
"verbose",
|
||||
"HandleRemoteSpellCast name=%s spellId=%s elapsed=%.2f expiresIn=%.2f charges=%s/%s",
|
||||
tostring(playerName),
|
||||
tostring(spellId),
|
||||
tonumber(elapsed) or 0,
|
||||
tonumber(expiresIn) or 0,
|
||||
hasCharges and tostring(currentCharges) or "-",
|
||||
hasCharges and tostring(maxChargeCount) or "-"
|
||||
)
|
||||
|
||||
self._cdNonce = (self._cdNonce or 0) + 1
|
||||
local nonce = self._cdNonce
|
||||
|
||||
self:SetActiveCooldown(playerName, spellId, {
|
||||
startTime = startTime,
|
||||
duration = duration,
|
||||
spellEntry = spellEntry,
|
||||
currentCharges = hasCharges and currentCharges or nil,
|
||||
maxCharges = hasCharges and maxChargeCount or nil,
|
||||
chargeStart = hasCharges and chargeStartTime or nil,
|
||||
chargeDuration = hasCharges and chargeDur or nil,
|
||||
lastCastTimestamp = castTimestamp,
|
||||
_nonce = nonce,
|
||||
})
|
||||
|
||||
self:LogTrackedSpellCast(playerName, spellEntry, {
|
||||
cooldown = effectiveCd,
|
||||
currentCharges = hasCharges and currentCharges or nil,
|
||||
maxCharges = hasCharges and maxChargeCount or nil,
|
||||
chargeCooldown = hasCharges and chargeDur or nil,
|
||||
})
|
||||
|
||||
if expiresIn > 0 then
|
||||
self:ScheduleTimer(function()
|
||||
local current = self:GetActiveCooldown(playerName, spellId)
|
||||
if current and current._nonce == nonce then
|
||||
self:ClearActiveCooldown(playerName, spellId)
|
||||
self:TriggerTrackerUpdate()
|
||||
end
|
||||
end, expiresIn)
|
||||
end
|
||||
|
||||
self:TriggerTrackerUpdate()
|
||||
end
|
||||
@@ -94,25 +94,6 @@ local PARTY_FRAME_PATTERNS = {
|
||||
|
||||
local unitFrameCache = {}
|
||||
|
||||
local function EntryNeedsVisualTicker(entry)
|
||||
if type(entry) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
local remaining = tonumber(entry.remaining) or 0
|
||||
if remaining > 0 then
|
||||
return true
|
||||
end
|
||||
|
||||
local maxCharges = tonumber(entry.maxCharges) or 0
|
||||
local currentCharges = tonumber(entry.currentCharges)
|
||||
if maxCharges > 0 and currentCharges ~= nil and currentCharges < maxCharges then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function BuildAnchorLayoutSignature(settings, ordered, unitByPlayer)
|
||||
local parts = {
|
||||
settings.attachToPartyFrame == true and "attach" or "stack",
|
||||
@@ -222,55 +203,6 @@ local function ResolveUnitAnchorFrame(unitId)
|
||||
return nil
|
||||
end
|
||||
|
||||
local function GetGroupPlayers(tracker)
|
||||
local players = {}
|
||||
|
||||
local ownName = HMGT:NormalizePlayerName(UnitName("player"))
|
||||
local ownClass = select(2, UnitClass("player"))
|
||||
local includeOwnPlayer = true
|
||||
if IsGroupTracker(tracker) then
|
||||
includeOwnPlayer = tracker.includeSelfFrame == true
|
||||
end
|
||||
if includeOwnPlayer then
|
||||
players[#players + 1] = {
|
||||
name = ownName,
|
||||
class = ownClass,
|
||||
isOwn = true,
|
||||
unitId = "player",
|
||||
}
|
||||
end
|
||||
|
||||
if IsInRaid() then
|
||||
for i = 1, GetNumGroupMembers() do
|
||||
local unitId = "raid" .. i
|
||||
local name = HMGT:NormalizePlayerName(UnitName(unitId))
|
||||
local class = select(2, UnitClass(unitId))
|
||||
if name and name ~= ownName then
|
||||
players[#players + 1] = {
|
||||
name = name,
|
||||
class = class,
|
||||
unitId = unitId,
|
||||
}
|
||||
end
|
||||
end
|
||||
elseif IsInGroup() then
|
||||
for i = 1, GetNumGroupMembers() - 1 do
|
||||
local unitId = "party" .. i
|
||||
local name = HMGT:NormalizePlayerName(UnitName(unitId))
|
||||
local class = select(2, UnitClass(unitId))
|
||||
if name and name ~= ownName then
|
||||
players[#players + 1] = {
|
||||
name = name,
|
||||
class = class,
|
||||
unitId = unitId,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return players
|
||||
end
|
||||
|
||||
local function GetTrackerLabel(tracker)
|
||||
if type(tracker) ~= "table" then
|
||||
return "Tracker"
|
||||
@@ -287,136 +219,6 @@ local function GetTrackerLabel(tracker)
|
||||
return "Tracker"
|
||||
end
|
||||
|
||||
local function GetTrackerSpellPool(categories)
|
||||
if HMGT_SpellData and type(HMGT_SpellData.GetSpellPoolForCategories) == "function" then
|
||||
return HMGT_SpellData.GetSpellPoolForCategories(categories)
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
local function GetTrackerSpellsForPlayer(classToken, specIndex, categories)
|
||||
if HMGT_SpellData and type(HMGT_SpellData.GetSpellsForCategories) == "function" then
|
||||
return HMGT_SpellData.GetSpellsForCategories(classToken, specIndex, categories)
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
local function CollectEntriesForPlayer(tracker, playerInfo)
|
||||
local entries = {}
|
||||
if type(tracker) ~= "table" or type(playerInfo) ~= "table" then
|
||||
return entries
|
||||
end
|
||||
|
||||
local playerName = playerInfo.name
|
||||
if not playerName then
|
||||
return entries
|
||||
end
|
||||
|
||||
local pData = HMGT.playerData[playerName]
|
||||
local classToken = pData and pData.class or playerInfo.class
|
||||
if not classToken then
|
||||
return entries
|
||||
end
|
||||
|
||||
local specIndex
|
||||
if playerInfo.isOwn then
|
||||
specIndex = GetSpecialization()
|
||||
if not specIndex or specIndex == 0 then
|
||||
return entries
|
||||
end
|
||||
else
|
||||
specIndex = pData and pData.specIndex or nil
|
||||
if not specIndex or tonumber(specIndex) <= 0 then
|
||||
return entries
|
||||
end
|
||||
end
|
||||
|
||||
local talents = pData and pData.talents or {}
|
||||
local spells = GetTrackerSpellsForPlayer(classToken, specIndex, tracker.categories)
|
||||
for _, spellEntry in ipairs(spells) do
|
||||
if tracker.enabledSpells[spellEntry.spellId] ~= false then
|
||||
local remaining, total, currentCharges, maxCharges = HMGT:GetCooldownInfo(playerName, spellEntry.spellId)
|
||||
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
local isAvailabilitySpell = HMGT:IsAvailabilitySpell(spellEntry)
|
||||
local include = HMGT:ShouldDisplayEntry(tracker, remaining, currentCharges, maxCharges, spellEntry)
|
||||
local spellKnown = HMGT:IsTrackedSpellKnownForPlayer(playerName, spellEntry.spellId)
|
||||
local hasPartialCharges = (tonumber(maxCharges) or 0) > 0
|
||||
and (tonumber(currentCharges) or tonumber(maxCharges) or 0) < (tonumber(maxCharges) or 0)
|
||||
local hasActiveCd = ((remaining or 0) > 0) or hasPartialCharges
|
||||
|
||||
if not spellKnown and not hasActiveCd then
|
||||
include = false
|
||||
end
|
||||
if isAvailabilitySpell and not spellKnown then
|
||||
include = false
|
||||
end
|
||||
if not playerInfo.isOwn and isAvailabilitySpell and not HMGT:HasAvailabilityState(playerName, spellEntry.spellId) then
|
||||
include = false
|
||||
end
|
||||
|
||||
if include then
|
||||
entries[#entries + 1] = {
|
||||
playerName = playerName,
|
||||
class = classToken,
|
||||
spellEntry = spellEntry,
|
||||
remaining = remaining,
|
||||
total = total > 0 and total or effectiveCd,
|
||||
currentCharges = currentCharges,
|
||||
maxCharges = maxCharges,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return entries
|
||||
end
|
||||
|
||||
local function CopyEntriesForPreview(entries, playerName)
|
||||
local copies = {}
|
||||
for _, entry in ipairs(entries or {}) do
|
||||
local nextEntry = {}
|
||||
for key, value in pairs(entry) do
|
||||
nextEntry[key] = value
|
||||
end
|
||||
nextEntry.playerName = playerName
|
||||
copies[#copies + 1] = nextEntry
|
||||
end
|
||||
return copies
|
||||
end
|
||||
|
||||
local function GetAvailablePartyPreviewUnits()
|
||||
local units = {}
|
||||
for index = 1, 4 do
|
||||
local unitId = "party" .. index
|
||||
if ResolveUnitAnchorFrame(unitId) then
|
||||
units[#units + 1] = {
|
||||
playerName = string.format("Party %d", index),
|
||||
unitId = unitId,
|
||||
}
|
||||
end
|
||||
end
|
||||
return units
|
||||
end
|
||||
|
||||
local function BuildPartyPreviewEntries(entries)
|
||||
local byPlayer = {}
|
||||
local order = {}
|
||||
local unitByPlayer = {}
|
||||
local previewUnits = GetAvailablePartyPreviewUnits()
|
||||
|
||||
for _, previewUnit in ipairs(previewUnits) do
|
||||
local playerName = previewUnit.playerName
|
||||
local playerEntries = CopyEntriesForPreview(entries, playerName)
|
||||
if #playerEntries > 0 then
|
||||
byPlayer[playerName] = playerEntries
|
||||
order[#order + 1] = playerName
|
||||
unitByPlayer[playerName] = previewUnit.unitId
|
||||
end
|
||||
end
|
||||
|
||||
return byPlayer, order, unitByPlayer, #order > 0
|
||||
end
|
||||
|
||||
local function SortTrackers(trackers)
|
||||
table.sort(trackers, function(a, b)
|
||||
local aId = tonumber(a and a.id) or 0
|
||||
@@ -467,13 +269,7 @@ Manager._shared.ShortName = ShortName
|
||||
Manager._shared.BuildAnchorLayoutSignature = BuildAnchorLayoutSignature
|
||||
Manager._shared.IsGroupTracker = IsGroupTracker
|
||||
Manager._shared.ResolveUnitAnchorFrame = ResolveUnitAnchorFrame
|
||||
Manager._shared.GetGroupPlayers = GetGroupPlayers
|
||||
Manager._shared.GetTrackerLabel = GetTrackerLabel
|
||||
Manager._shared.GetTrackerSpellPool = GetTrackerSpellPool
|
||||
Manager._shared.GetTrackerSpellsForPlayer = GetTrackerSpellsForPlayer
|
||||
Manager._shared.CollectEntriesForPlayer = CollectEntriesForPlayer
|
||||
Manager._shared.BuildPartyPreviewEntries = BuildPartyPreviewEntries
|
||||
Manager._shared.EntryNeedsVisualTicker = EntryNeedsVisualTicker
|
||||
Manager._shared.BuildGroupDisplaySignature = BuildGroupDisplaySignature
|
||||
|
||||
function Manager:GetTrackers()
|
||||
@@ -492,6 +288,13 @@ function Manager:GetTrackers()
|
||||
return self._trackerCache
|
||||
end
|
||||
|
||||
function Manager:GetTrackerFrameKey(tracker)
|
||||
if type(tracker) == "table" then
|
||||
return GetTrackerFrameKey(tracker.id)
|
||||
end
|
||||
return GetTrackerFrameKey(tracker)
|
||||
end
|
||||
|
||||
function Manager:MarkTrackersDirty()
|
||||
self._trackerCache = nil
|
||||
self._trackerCacheSignature = nil
|
||||
@@ -648,12 +451,8 @@ function Manager:RefreshVisibleVisuals()
|
||||
break
|
||||
end
|
||||
local entries = byPlayer[playerName] or {}
|
||||
if HMGT.FilterDisplayEntries then
|
||||
entries = HMGT:FilterDisplayEntries(tracker, entries) or entries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries)
|
||||
end
|
||||
local tickThis = false
|
||||
entries, tickThis = HMGT:FinalizeTrackerEntries(tracker, entries, tracker.trackerKey or nil)
|
||||
if #entries == 0 then
|
||||
needsFullRefresh = true
|
||||
break
|
||||
@@ -666,11 +465,8 @@ function Manager:RefreshVisibleVisuals()
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
||||
totalEntries = totalEntries + #entries
|
||||
byPlayerFiltered[playerName] = entries
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
if tickThis then
|
||||
shouldTick = true
|
||||
end
|
||||
end
|
||||
local newSignature = BuildGroupDisplaySignature(currentOrder, byPlayerFiltered)
|
||||
@@ -680,36 +476,29 @@ function Manager:RefreshVisibleVisuals()
|
||||
end
|
||||
end
|
||||
else
|
||||
local frame = self.frames[frameKey]
|
||||
if frame and frame:IsShown() then
|
||||
local entries, shouldShow = self:BuildEntriesForTracker(tracker)
|
||||
if not shouldShow then
|
||||
needsFullRefresh = true
|
||||
else
|
||||
if HMGT.FilterDisplayEntries then
|
||||
entries = HMGT:FilterDisplayEntries(tracker, entries) or entries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries)
|
||||
end
|
||||
if #entries == 0 then
|
||||
local frame = self.frames[frameKey]
|
||||
if frame and frame:IsShown() then
|
||||
local entries, shouldShow = self:BuildEntriesForTracker(tracker)
|
||||
if not shouldShow then
|
||||
needsFullRefresh = true
|
||||
else
|
||||
local tickThis = false
|
||||
entries, tickThis = HMGT:FinalizeTrackerEntries(tracker, entries, tracker.trackerKey or nil)
|
||||
if #entries == 0 then
|
||||
needsFullRefresh = true
|
||||
else
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
||||
totalEntries = totalEntries + #entries
|
||||
local newSignature = BuildNormalDisplaySignature(true, entries)
|
||||
if self._displaySignatures[frameKey] ~= newSignature then
|
||||
needsFullRefresh = true
|
||||
end
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
if self._displaySignatures[frameKey] ~= newSignature then
|
||||
needsFullRefresh = true
|
||||
end
|
||||
if tickThis then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -751,12 +540,8 @@ function Manager:UpdateDisplay()
|
||||
local entries, shouldShow = self:BuildEntriesForTracker(tracker)
|
||||
|
||||
if shouldShow then
|
||||
if HMGT.FilterDisplayEntries then
|
||||
entries = HMGT:FilterDisplayEntries(tracker, entries) or entries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries)
|
||||
end
|
||||
local tickThis = false
|
||||
entries, tickThis = HMGT:FinalizeTrackerEntries(tracker, entries, tracker.trackerKey or nil)
|
||||
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
||||
frame:Show()
|
||||
@@ -769,11 +554,8 @@ function Manager:UpdateDisplay()
|
||||
layoutDirty = true
|
||||
end
|
||||
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
if tickThis then
|
||||
shouldTick = true
|
||||
end
|
||||
else
|
||||
frame:Hide()
|
||||
|
||||
@@ -142,13 +142,26 @@ local function IsPartyAttachMode(tracker)
|
||||
end
|
||||
|
||||
local function IsGroupTracker(tracker)
|
||||
return type(tracker) == "table" and tracker.trackerType == "group"
|
||||
return HMGT.IsGroupTrackerConfig and HMGT:IsGroupTrackerConfig(tracker) or (type(tracker) == "table" and tracker.trackerType == "group")
|
||||
end
|
||||
|
||||
local TRACKER_TYPE_VALUES = {
|
||||
normal = L["OPT_TRACKER_TYPE_NORMAL"] or "Normal tracker",
|
||||
group = L["OPT_TRACKER_TYPE_GROUP"] or "Group-based tracker",
|
||||
}
|
||||
local function GetTrackerTypeValues()
|
||||
return HMGT.GetTrackerTypeOptions and HMGT:GetTrackerTypeOptions() or {
|
||||
normal = L["OPT_TRACKER_TYPE_NORMAL"] or "Normal tracker",
|
||||
group = L["OPT_TRACKER_TYPE_GROUP"] or "Group-based tracker",
|
||||
}
|
||||
end
|
||||
|
||||
local function GetPresetLabel(presetKey)
|
||||
local definition = HMGT.GetTrackerPresetDefinition and HMGT:GetTrackerPresetDefinition(presetKey) or nil
|
||||
if not definition then
|
||||
return tostring(presetKey or (L["OPT_TRACKER"] or "Tracker"))
|
||||
end
|
||||
if type(definition.defaultName) == "function" then
|
||||
return tostring(definition.defaultName(L))
|
||||
end
|
||||
return tostring(definition.defaultName or definition.moduleName or presetKey)
|
||||
end
|
||||
|
||||
local function GetTrackerVisibilitySummary(tracker)
|
||||
local parts = {}
|
||||
@@ -180,7 +193,7 @@ local function GetTrackerSummaryText(tracker)
|
||||
local display = tracker.showBar and (L["OPT_DISPLAY_BAR"] or "Progress bars") or (L["OPT_DISPLAY_ICON"] or "Icons")
|
||||
|
||||
return table.concat({
|
||||
string.format("|cffffd100%s|r: %s", L["OPT_TRACKER_TYPE"] or "Tracker type", TRACKER_TYPE_VALUES[tracker.trackerType or "normal"] or (L["OPT_TRACKER_TYPE_NORMAL"] or "Normal tracker")),
|
||||
string.format("|cffffd100%s|r: %s", L["OPT_TRACKER_TYPE"] or "Tracker type", GetTrackerTypeValues()[tracker.trackerType or "normal"] or (L["OPT_TRACKER_TYPE_NORMAL"] or "Normal tracker")),
|
||||
string.format("|cffffd100%s|r: %s", L["OPT_TRACKER_CATEGORIES"] or "Categories", GetTrackerCategoriesSummary(tracker)),
|
||||
string.format("|cffffd100%s|r: %s", L["OPT_STATUS_MODE"] or "Mode", modeLabel),
|
||||
string.format("|cffffd100%s|r: %s", L["OPT_STATUS_DISPLAY"] or "Display", display),
|
||||
@@ -814,7 +827,7 @@ local function BuildGlobalSpellBrowserArgs()
|
||||
end
|
||||
|
||||
local function BuildTrackerOverviewArgs()
|
||||
return {
|
||||
local args = {
|
||||
description = {
|
||||
type = "description",
|
||||
order = 1,
|
||||
@@ -833,22 +846,36 @@ local function BuildTrackerOverviewArgs()
|
||||
return string.format("%s\n\n%s (%d): %s", body, L["OPT_TRACKERS"] or "Tracker Bars", #trackers, table.concat(names, ", "))
|
||||
end,
|
||||
},
|
||||
addTracker = {
|
||||
}
|
||||
|
||||
local definitions = HMGT.GetTrackerPresetDefinitions and HMGT:GetTrackerPresetDefinitions() or {}
|
||||
local presetKeys = {}
|
||||
for presetKey in pairs(definitions) do
|
||||
presetKeys[#presetKeys + 1] = presetKey
|
||||
end
|
||||
table.sort(presetKeys, function(a, b)
|
||||
return GetPresetLabel(a) < GetPresetLabel(b)
|
||||
end)
|
||||
|
||||
for index, presetKey in ipairs(presetKeys) do
|
||||
args["addPreset_" .. presetKey] = {
|
||||
type = "execute",
|
||||
order = 2,
|
||||
order = 2 + index,
|
||||
width = "full",
|
||||
name = L["OPT_ADD_TRACKER"] or "Add tracker",
|
||||
name = function()
|
||||
return string.format("%s: %s", L["OPT_ADD_TRACKER"] or "Add tracker", GetPresetLabel(presetKey))
|
||||
end,
|
||||
func = function()
|
||||
local nextId = HMGT:GetNextTrackerId()
|
||||
local tracker = HMGT:CreateTrackerConfig(nextId, {
|
||||
name = string.format("%s %d", L["OPT_TRACKER"] or "Tracker", nextId),
|
||||
})
|
||||
local tracker = HMGT:BuildTrackerConfigFromPreset(presetKey, nextId)
|
||||
HMGT.db.profile.trackers = HMGT.db.profile.trackers or {}
|
||||
HMGT.db.profile.trackers[#HMGT.db.profile.trackers + 1] = tracker
|
||||
TriggerTrackerUpdate(true)
|
||||
end,
|
||||
},
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
return args
|
||||
end
|
||||
|
||||
local function BuildTrackerGroup(trackerId, order)
|
||||
@@ -1008,7 +1035,7 @@ local function BuildTrackerGroup(trackerId, order)
|
||||
width = "full",
|
||||
name = L["OPT_TRACKER_TYPE"] or "Tracker type",
|
||||
desc = L["OPT_TRACKER_TYPE_DESC"] or "Choose whether this tracker uses one shared frame or separate frames per group member.",
|
||||
values = TRACKER_TYPE_VALUES,
|
||||
values = GetTrackerTypeValues,
|
||||
get = function()
|
||||
local tracker = s()
|
||||
return (tracker and tracker.trackerType) or "normal"
|
||||
|
||||
65
Modules/Tracker/TrackerPlayerState.lua
Normal file
65
Modules/Tracker/TrackerPlayerState.lua
Normal file
@@ -0,0 +1,65 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
HMGT.TrackerPlayerState = HMGT.TrackerPlayerState or {}
|
||||
|
||||
local internals = HMGT.TrackerInternals or {}
|
||||
local IsSpellKnownLocally = internals.IsSpellKnownLocally
|
||||
|
||||
function HMGT:CollectOwnAvailableTrackerSpells(classToken, specIndex)
|
||||
local class = classToken or select(2, UnitClass("player"))
|
||||
local spec = tonumber(specIndex) or tonumber(GetSpecialization())
|
||||
if not class or not spec or spec <= 0 then
|
||||
return {}
|
||||
end
|
||||
if not HMGT_SpellData or type(HMGT_SpellData.GetSpellsForSpec) ~= "function" then
|
||||
return {}
|
||||
end
|
||||
|
||||
local knownSpells = {}
|
||||
for _, datasetName in ipairs({ "Interrupts", "RaidCooldowns", "GroupCooldowns" }) do
|
||||
local dataset = HMGT_SpellData[datasetName]
|
||||
if type(dataset) == "table" then
|
||||
local spells = HMGT_SpellData.GetSpellsForSpec(class, spec, dataset)
|
||||
for _, entry in ipairs(spells) do
|
||||
local sid = tonumber(entry.spellId)
|
||||
if sid and sid > 0 and IsSpellKnownLocally and IsSpellKnownLocally(sid) then
|
||||
knownSpells[sid] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local ownName = self:NormalizePlayerName(UnitName("player"))
|
||||
local ownCDs = ownName and self:GetPlayerCooldownMap(ownName, false)
|
||||
if ownCDs then
|
||||
for sid in pairs(ownCDs) do
|
||||
sid = tonumber(sid)
|
||||
if sid and sid > 0 then
|
||||
knownSpells[sid] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
return knownSpells
|
||||
end
|
||||
|
||||
function HMGT:IsTrackedSpellKnownForPlayer(playerName, spellId)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid or sid <= 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
local ownName = self:NormalizePlayerName(UnitName("player"))
|
||||
local pData = normalizedName and self.playerData[normalizedName]
|
||||
if pData and type(pData.knownSpells) == "table" and pData.knownSpells[sid] == true then
|
||||
return true
|
||||
end
|
||||
|
||||
if normalizedName and ownName and normalizedName == ownName and IsSpellKnownLocally then
|
||||
return IsSpellKnownLocally(sid)
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
410
Modules/Tracker/TrackerState.lua
Normal file
410
Modules/Tracker/TrackerState.lua
Normal file
@@ -0,0 +1,410 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
HMGT.TrackerState = HMGT.TrackerState or {}
|
||||
|
||||
function HMGT:EnsureTrackerStateTables()
|
||||
self.playerData = self.playerData or {}
|
||||
self.activeCDs = self.activeCDs or {}
|
||||
self.availabilityStates = self.availabilityStates or {}
|
||||
self.localSpellStateRevisions = self.localSpellStateRevisions or {}
|
||||
self.remoteSpellStateRevisions = self.remoteSpellStateRevisions or {}
|
||||
self.knownChargeInfo = self.knownChargeInfo or {}
|
||||
end
|
||||
|
||||
function HMGT:ResetTrackerState()
|
||||
self.playerData = {}
|
||||
self.activeCDs = {}
|
||||
self.availabilityStates = {}
|
||||
self.localSpellStateRevisions = {}
|
||||
self.remoteSpellStateRevisions = {}
|
||||
self.knownChargeInfo = {}
|
||||
end
|
||||
|
||||
function HMGT:GetPlayerCooldownMap(playerName, create)
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
if not normalizedName then
|
||||
return nil
|
||||
end
|
||||
self:EnsureTrackerStateTables()
|
||||
if create then
|
||||
self.activeCDs[normalizedName] = self.activeCDs[normalizedName] or {}
|
||||
end
|
||||
return self.activeCDs[normalizedName]
|
||||
end
|
||||
|
||||
function HMGT:GetAvailabilityStateMap(playerName, create)
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
if not normalizedName then
|
||||
return nil
|
||||
end
|
||||
self:EnsureTrackerStateTables()
|
||||
if create then
|
||||
self.availabilityStates[normalizedName] = self.availabilityStates[normalizedName] or {}
|
||||
end
|
||||
return self.availabilityStates[normalizedName]
|
||||
end
|
||||
|
||||
function HMGT:GetAvailabilityStateEntry(playerName, spellId)
|
||||
local sid = tonumber(spellId)
|
||||
local states = self:GetAvailabilityStateMap(playerName, false)
|
||||
return states and sid and states[sid] or nil
|
||||
end
|
||||
|
||||
function HMGT:SetAvailabilityStateEntry(playerName, spellId, stateData)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid or sid <= 0 or type(stateData) ~= "table" then
|
||||
return nil
|
||||
end
|
||||
local states = self:GetAvailabilityStateMap(playerName, true)
|
||||
if not states then
|
||||
return nil
|
||||
end
|
||||
states[sid] = stateData
|
||||
return stateData
|
||||
end
|
||||
|
||||
function HMGT:ClearAvailabilityState(playerName, spellId)
|
||||
local sid = tonumber(spellId)
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
if not normalizedName or not sid or sid <= 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
local states = self.availabilityStates and self.availabilityStates[normalizedName]
|
||||
if not states or not states[sid] then
|
||||
return false
|
||||
end
|
||||
|
||||
states[sid] = nil
|
||||
if not next(states) then
|
||||
self.availabilityStates[normalizedName] = nil
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function HMGT:GetActiveCooldown(playerName, spellId)
|
||||
local sid = tonumber(spellId)
|
||||
local cooldowns = self:GetPlayerCooldownMap(playerName, false)
|
||||
return cooldowns and sid and cooldowns[sid] or nil
|
||||
end
|
||||
|
||||
function HMGT:SetActiveCooldown(playerName, spellId, cdData)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid or sid <= 0 or type(cdData) ~= "table" then
|
||||
return nil
|
||||
end
|
||||
local cooldowns = self:GetPlayerCooldownMap(playerName, true)
|
||||
if not cooldowns then
|
||||
return nil
|
||||
end
|
||||
cooldowns[sid] = cdData
|
||||
return cdData
|
||||
end
|
||||
|
||||
function HMGT:ClearActiveCooldown(playerName, spellId)
|
||||
local sid = tonumber(spellId)
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
if not normalizedName or not sid or sid <= 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
local cooldowns = self.activeCDs and self.activeCDs[normalizedName]
|
||||
if not cooldowns or not cooldowns[sid] then
|
||||
return false
|
||||
end
|
||||
|
||||
cooldowns[sid] = nil
|
||||
if not next(cooldowns) then
|
||||
self.activeCDs[normalizedName] = nil
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function HMGT:ClearPlayerCooldowns(playerName)
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
if not normalizedName then
|
||||
return false
|
||||
end
|
||||
if self.activeCDs and self.activeCDs[normalizedName] then
|
||||
self.activeCDs[normalizedName] = nil
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function HMGT:GetLocalSpellStateRevision(spellId)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid or sid <= 0 then
|
||||
return 0
|
||||
end
|
||||
self:EnsureTrackerStateTables()
|
||||
return tonumber(self.localSpellStateRevisions[sid]) or 0
|
||||
end
|
||||
|
||||
function HMGT:EnsureLocalSpellStateRevision(spellId)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid or sid <= 0 then
|
||||
return 0
|
||||
end
|
||||
self:EnsureTrackerStateTables()
|
||||
local current = tonumber(self.localSpellStateRevisions[sid]) or 0
|
||||
if current <= 0 then
|
||||
current = 1
|
||||
self.localSpellStateRevisions[sid] = current
|
||||
end
|
||||
return current
|
||||
end
|
||||
|
||||
function HMGT:NextLocalSpellStateRevision(spellId)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid or sid <= 0 then
|
||||
return 0
|
||||
end
|
||||
self:EnsureTrackerStateTables()
|
||||
local nextRevision = (tonumber(self.localSpellStateRevisions[sid]) or 0) + 1
|
||||
self.localSpellStateRevisions[sid] = nextRevision
|
||||
return nextRevision
|
||||
end
|
||||
|
||||
function HMGT:GetRemoteSpellStateRevision(playerName, spellId)
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
local sid = tonumber(spellId)
|
||||
local bySpell = normalizedName and self.remoteSpellStateRevisions[normalizedName]
|
||||
return tonumber(bySpell and bySpell[sid]) or 0
|
||||
end
|
||||
|
||||
function HMGT:SetRemoteSpellStateRevision(playerName, spellId, revision)
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
local sid = tonumber(spellId)
|
||||
local rev = tonumber(revision) or 0
|
||||
if not normalizedName or not sid or sid <= 0 or rev <= 0 then
|
||||
return
|
||||
end
|
||||
self:EnsureTrackerStateTables()
|
||||
self.remoteSpellStateRevisions[normalizedName] = self.remoteSpellStateRevisions[normalizedName] or {}
|
||||
self.remoteSpellStateRevisions[normalizedName][sid] = rev
|
||||
end
|
||||
|
||||
function HMGT:ClearRemoteSpellStateRevisions(playerName)
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
if not normalizedName then
|
||||
return false
|
||||
end
|
||||
if self.remoteSpellStateRevisions and self.remoteSpellStateRevisions[normalizedName] then
|
||||
self.remoteSpellStateRevisions[normalizedName] = nil
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function HMGT:ClearTrackerStateForPlayer(playerName)
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
if not normalizedName then
|
||||
return false
|
||||
end
|
||||
|
||||
local changed = false
|
||||
if self.activeCDs and self.activeCDs[normalizedName] then
|
||||
self.activeCDs[normalizedName] = nil
|
||||
changed = true
|
||||
end
|
||||
if self.availabilityStates and self.availabilityStates[normalizedName] then
|
||||
self.availabilityStates[normalizedName] = nil
|
||||
changed = true
|
||||
end
|
||||
if self.remoteSpellStateRevisions and self.remoteSpellStateRevisions[normalizedName] then
|
||||
self.remoteSpellStateRevisions[normalizedName] = nil
|
||||
changed = true
|
||||
end
|
||||
|
||||
return changed
|
||||
end
|
||||
|
||||
function HMGT:StoreKnownChargeInfo(spellId, maxCharges, chargeDuration)
|
||||
local sid = tonumber(spellId)
|
||||
local maxCount = tonumber(maxCharges)
|
||||
if not sid or sid <= 0 or not maxCount or maxCount <= 1 then
|
||||
return
|
||||
end
|
||||
|
||||
self:EnsureTrackerStateTables()
|
||||
self.knownChargeInfo[sid] = {
|
||||
maxCharges = math.max(1, math.floor(maxCount + 0.5)),
|
||||
chargeDuration = math.max(0, tonumber(chargeDuration) or 0),
|
||||
updatedAt = GetTime(),
|
||||
}
|
||||
end
|
||||
|
||||
function HMGT:GetKnownChargeInfo(spellEntry, talents, spellId, fallbackChargeDuration)
|
||||
local sid = tonumber(spellId or (spellEntry and spellEntry.spellId))
|
||||
if not sid or sid <= 0 then
|
||||
return 0, 0
|
||||
end
|
||||
|
||||
local cached = self.knownChargeInfo and self.knownChargeInfo[sid]
|
||||
local cachedMax = tonumber(cached and cached.maxCharges) or 0
|
||||
local cachedDuration = tonumber(cached and cached.chargeDuration) or 0
|
||||
|
||||
local inferredMax, inferredDuration = HMGT_SpellData.GetEffectiveChargeInfo(
|
||||
spellEntry,
|
||||
talents or {},
|
||||
(cachedMax > 0) and cachedMax or nil,
|
||||
(cachedDuration > 0) and cachedDuration or fallbackChargeDuration
|
||||
)
|
||||
|
||||
local maxCharges = math.max(cachedMax, tonumber(inferredMax) or 0)
|
||||
local chargeDuration = math.max(
|
||||
tonumber(inferredDuration) or 0,
|
||||
cachedDuration,
|
||||
tonumber(fallbackChargeDuration) or 0
|
||||
)
|
||||
|
||||
if maxCharges > 1 then
|
||||
self:StoreKnownChargeInfo(sid, maxCharges, chargeDuration)
|
||||
end
|
||||
|
||||
return maxCharges, chargeDuration
|
||||
end
|
||||
|
||||
function HMGT:PruneAvailabilityStates(playerName, knownSpells)
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
local states = normalizedName and self.availabilityStates[normalizedName]
|
||||
if not states or type(knownSpells) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
local changed = false
|
||||
for sid in pairs(states) do
|
||||
if not knownSpells[tonumber(sid)] then
|
||||
states[sid] = nil
|
||||
changed = true
|
||||
end
|
||||
end
|
||||
|
||||
if not next(states) then
|
||||
self.availabilityStates[normalizedName] = nil
|
||||
end
|
||||
return changed
|
||||
end
|
||||
|
||||
function HMGT:ResolveChargeState(cdData, now)
|
||||
if type(cdData) ~= "table" then
|
||||
return 0, 0, 0, 0
|
||||
end
|
||||
|
||||
now = tonumber(now) or GetTime()
|
||||
local maxCharges = math.max(0, tonumber(cdData.maxCharges) or 0)
|
||||
local currentCharges = math.max(0, tonumber(cdData.currentCharges) or 0)
|
||||
local chargeDuration = math.max(0, tonumber(cdData.chargeDuration) or 0)
|
||||
local chargeStart = tonumber(cdData.chargeStart)
|
||||
|
||||
if maxCharges <= 0 then
|
||||
return 0, chargeDuration, currentCharges, maxCharges
|
||||
end
|
||||
if currentCharges >= maxCharges or chargeDuration <= 0 or not chargeStart then
|
||||
return 0, chargeDuration, math.min(currentCharges, maxCharges), maxCharges
|
||||
end
|
||||
|
||||
local elapsed = math.max(0, now - chargeStart)
|
||||
local gainedCharges = math.floor(elapsed / chargeDuration)
|
||||
local remaining = chargeDuration - (elapsed % chargeDuration)
|
||||
|
||||
if gainedCharges > 0 then
|
||||
currentCharges = math.min(maxCharges, currentCharges + gainedCharges)
|
||||
if currentCharges >= maxCharges then
|
||||
currentCharges = maxCharges
|
||||
chargeStart = nil
|
||||
remaining = 0
|
||||
else
|
||||
chargeStart = now - (elapsed % chargeDuration)
|
||||
end
|
||||
|
||||
cdData.currentCharges = currentCharges
|
||||
cdData.chargeStart = chargeStart
|
||||
if currentCharges >= maxCharges then
|
||||
cdData.startTime = now
|
||||
cdData.duration = 0
|
||||
else
|
||||
local missing = maxCharges - currentCharges
|
||||
cdData.startTime = chargeStart
|
||||
cdData.duration = missing * chargeDuration
|
||||
end
|
||||
end
|
||||
|
||||
if currentCharges >= maxCharges then
|
||||
return 0, chargeDuration, currentCharges, maxCharges
|
||||
end
|
||||
return math.max(0, remaining), chargeDuration, currentCharges, maxCharges
|
||||
end
|
||||
|
||||
function HMGT:RefreshCooldownExpiryTimer(playerName, spellId, cdData)
|
||||
if not cdData then return 0 end
|
||||
local now = GetTime()
|
||||
local duration = tonumber(cdData.duration) or 0
|
||||
local startTime = tonumber(cdData.startTime) or now
|
||||
local expiresIn = math.max(0, duration - (now - startTime))
|
||||
|
||||
self._cdNonce = (self._cdNonce or 0) + 1
|
||||
local nonce = self._cdNonce
|
||||
cdData._nonce = nonce
|
||||
|
||||
if expiresIn > 0 then
|
||||
self:ScheduleTimer(function()
|
||||
local current = self:GetActiveCooldown(playerName, spellId)
|
||||
if current and current._nonce == nonce then
|
||||
self:ClearActiveCooldown(playerName, spellId)
|
||||
if playerName == self:NormalizePlayerName(UnitName("player")) then
|
||||
self:PublishOwnSpellState(spellId)
|
||||
end
|
||||
self:TriggerTrackerUpdate()
|
||||
end
|
||||
end, expiresIn)
|
||||
end
|
||||
return expiresIn
|
||||
end
|
||||
|
||||
function HMGT:CleanupStaleCooldowns()
|
||||
local now = GetTime()
|
||||
local ownName = self:NormalizePlayerName(UnitName("player"))
|
||||
local removed = 0
|
||||
for playerName, spells in pairs(self.activeCDs) do
|
||||
for spellId, cdInfo in pairs(spells) do
|
||||
local duration = tonumber(cdInfo.duration) or 0
|
||||
local startTime = tonumber(cdInfo.startTime) or now
|
||||
local rem = duration - (now - startTime)
|
||||
local hasCharges = (tonumber(cdInfo.maxCharges) or 0) > 0
|
||||
local currentCharges = tonumber(cdInfo.currentCharges) or 0
|
||||
local maxCharges = tonumber(cdInfo.maxCharges) or 0
|
||||
if hasCharges then
|
||||
local _, _, cur, max = self:ResolveChargeState(cdInfo, now)
|
||||
currentCharges = cur
|
||||
maxCharges = max
|
||||
end
|
||||
local shouldDrop = false
|
||||
if hasCharges then
|
||||
if currentCharges >= maxCharges then
|
||||
shouldDrop = true
|
||||
elseif (tonumber(cdInfo.chargeDuration) or 0) <= 0 and rem <= -2 then
|
||||
shouldDrop = true
|
||||
end
|
||||
elseif rem <= -2 then
|
||||
shouldDrop = true
|
||||
end
|
||||
if shouldDrop then
|
||||
spells[spellId] = nil
|
||||
if playerName == ownName then
|
||||
self:PublishOwnSpellState(spellId)
|
||||
end
|
||||
removed = removed + 1
|
||||
end
|
||||
end
|
||||
if not next(spells) then
|
||||
self.activeCDs[playerName] = nil
|
||||
end
|
||||
end
|
||||
if removed > 0 then
|
||||
self:Debug("verbose", "CleanupStaleCooldowns removed=%d", removed)
|
||||
end
|
||||
end
|
||||
1046
Modules/Tracker/TrackerSync.lua
Normal file
1046
Modules/Tracker/TrackerSync.lua
Normal file
File diff suppressed because it is too large
Load Diff
23
readme.md
23
readme.md
@@ -5,8 +5,8 @@ It combines cooldown tracking, encounter reminders, notes, and map utilities in
|
||||
|
||||
## Status
|
||||
|
||||
- Stable version: `1.3`
|
||||
- Current build: `2.0-beta`
|
||||
- Stable version: `2.0.0`
|
||||
- Current build: `2.1.0-beta`
|
||||
- SavedVariables: `HailMaryGuildToolsDB`
|
||||
|
||||
## Main Features
|
||||
@@ -16,7 +16,6 @@ It combines cooldown tracking, encounter reminders, notes, and map utilities in
|
||||
- Per-tracker bar and icon layouts
|
||||
- Aura Expiry for selected buffs and channels
|
||||
- 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
|
||||
- Version mismatch detection inside groups and raids
|
||||
- Blizzard AddOn options integration with Ace3-based module configuration
|
||||
@@ -59,12 +58,24 @@ Provides a dedicated notes window for raid notes, personal notes, and drafts.
|
||||
Toggles tracker test mode
|
||||
- `/hmgt notes`
|
||||
Opens the notes window
|
||||
- `/hmgt lura`
|
||||
Opens the L'ura rune helper
|
||||
- `/hmgt lura circle|x|diamond|t|triangle`
|
||||
Adds one rune to the next L'ura sequence slot; slot 5 sends the sequence for raid leader/assist
|
||||
- `Encounter Alerts > L'ura Runes > Rune action bar`
|
||||
Shows five clickable rune buttons plus a local clear button for building the sequence
|
||||
- `/hmgt lura reset`
|
||||
Clears the local L'ura sequence builder
|
||||
- `/hmgt lura bar`
|
||||
Toggles the L'ura rune action bar
|
||||
- `/hmgt debug`
|
||||
Opens the developer tools window
|
||||
Opens the debug console
|
||||
- `/hmgt dev`
|
||||
Alias for the developer tools window
|
||||
Alias for the debug console
|
||||
- `/hmgt status`
|
||||
Prints a compact addon health check
|
||||
- `/hmgt version`
|
||||
Opens the version window when developer tools are enabled
|
||||
Opens the version window when the debug console is enabled
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
Reference in New Issue
Block a user