Files

1114 lines
35 KiB
Lua

local ADDON_NAME = "HailMaryGuildTools"
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
if not HMGT then return end
local EA = HMGT.EncounterAlerts
if not EA then return end
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME, true) or {}
local AceConfigRegistry = LibStub("AceConfigRegistry-3.0", true)
local LR = {}
EA.LuraRunes = LR
local MEDIA_DIR = "Interface\\AddOns\\HailMaryGuildTools\\Modules\\EncounterAlerts\\Media\\LuraRunes\\"
local FALLBACK_TEXTURE = "Interface\\AddOns\\HailMaryGuildTools\\Media\\HailMaryIcon.png"
local ROLE_ICON_TEXTURE = "Interface\\LFGFrame\\UI-LFG-ICON-ROLES"
local CLEAR_BUTTON_TEXTURE = "Interface\\Buttons\\UI-GroupLoot-Pass-Up"
local RUNE_ORDER = { "circle", "cross", "diamond", "t", "triangle" }
local RUNE_DATA = {
circle = {
label = "Circle",
chatToken = "Rune_Circle",
texture = MEDIA_DIR .. "Rune_Circle.tga",
aliases = { "kreis", "round", "rund" },
},
cross = {
label = "Cross",
chatToken = "Rune_X",
texture = MEDIA_DIR .. "Rune_X.tga",
aliases = { "x", "kreuz" },
},
diamond = {
label = "Diamond",
chatToken = "Rune_Diamond",
texture = MEDIA_DIR .. "Rune_Diamond.tga",
aliases = { "diamant", "rhombus" },
},
t = {
label = "T",
chatToken = "Rune_T",
texture = MEDIA_DIR .. "Rune_T.tga",
aliases = { "tee" },
},
triangle = {
label = "Triangle",
chatToken = "Rune_Triangle",
texture = MEDIA_DIR .. "Rune_Triangle.tga",
aliases = { "dreieck" },
},
}
local RUNE_ALIASES = {}
for key, data in pairs(RUNE_DATA) do
RUNE_ALIASES[key] = key
if data.chatToken then
RUNE_ALIASES[string.lower(data.chatToken)] = key
end
for _, alias in ipairs(data.aliases or {}) do
RUNE_ALIASES[alias] = key
end
end
local DEFAULT_TEST_ASSIGNMENTS = {
"circle",
"cross",
"diamond",
"t",
"triangle",
}
local LURA_NAME_TOKENS = { "l'ura", "lura" }
local LURA_CONTEXT_MAP_IDS = {}
local LURA_ENCOUNTER_IDS = {}
local LURA_NPC_IDS = {}
local LURA_SCAN_UNITS = { "boss1", "boss2", "boss3", "boss4", "boss5", "target", "focus" }
local function Debug(level, fmt, ...)
if HMGT and HMGT.DebugScoped then
HMGT:DebugScoped(level or "info", "EncounterAlerts", fmt, ...)
end
end
local function NotifyOptionsChanged()
if AceConfigRegistry and type(AceConfigRegistry.NotifyChange) == "function" then
AceConfigRegistry:NotifyChange(ADDON_NAME)
end
end
local function ClampNumber(value, minimum, maximum, fallback)
local number = tonumber(value)
if not number then
number = fallback
end
number = tonumber(number) or 0
if number < minimum then return minimum end
if number > maximum then return maximum end
return number
end
local function NormalizeRuneKey(value)
local key = tostring(value or ""):lower()
return RUNE_ALIASES[key] or ""
end
local function ParseRuneRaidChatMessage(message)
local okText, text = pcall(tostring, message)
if not okText or type(text) ~= "string" then
return nil, nil
end
local okMatch, slotText, token = pcall(string.match, text, "^HMGT:Rune([1-5]):([%w_%-]+)$")
if not okMatch or not slotText or not token then
return nil, nil
end
local key = NormalizeRuneKey(token)
if key == "" then
return nil, nil
end
return tonumber(slotText), key
end
local function NormalizeActionBarOrientation(value)
if tostring(value or "") == "vertical" then
return "vertical"
end
return "horizontal"
end
local function TextLooksLikeLura(text)
local value = tostring(text or ""):lower()
if value == "" then
return false
end
for _, token in ipairs(LURA_NAME_TOKENS) do
if string.find(value, token, 1, true) then
return true
end
end
if string.find((value:gsub("%W", "")), "lura", 1, true) then
return true
end
return false
end
local function GetNPCIdFromGUID(guid)
local _, _, _, _, _, npcId = strsplit("-", tostring(guid or ""))
return tonumber(npcId)
end
local function UnitLooksLikeLura(unitId)
if not unitId or not UnitExists(unitId) then
return false
end
local npcId = GetNPCIdFromGUID(UnitGUID(unitId))
if npcId and LURA_NPC_IDS[npcId] then
return true
end
return TextLooksLikeLura(UnitName(unitId))
end
local function NormalizeColor(color, fallback)
color = type(color) == "table" and color or {}
fallback = type(fallback) == "table" and fallback or {}
return {
r = ClampNumber(color.r, 0, 1, fallback.r or 1),
g = ClampNumber(color.g, 0, 1, fallback.g or 0.82),
b = ClampNumber(color.b, 0, 1, fallback.b or 0.1),
a = ClampNumber(color.a, 0, 1, fallback.a or 0.9),
}
end
local function ApplyIconBorder(frame, settings, backgroundColor)
if not frame or type(frame.SetBackdrop) ~= "function" then
return
end
local border = settings and settings.border or {}
if border.enabled ~= true then
frame:SetBackdrop(nil)
return
end
local width = math.max(1, tonumber(border.width) or 2)
frame:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
edgeSize = width,
insets = { left = width, right = width, top = width, bottom = width },
})
local bg = backgroundColor or { r = 0, g = 0, b = 0, a = 0.35 }
local color = border.color or {}
frame:SetBackdropColor(bg.r or 0, bg.g or 0, bg.b or 0, bg.a or 0.35)
frame:SetBackdropBorderColor(color.r or 1, color.g or 0.82, color.b or 0.1, color.a or 0.9)
end
local function SetTankIconTexture(texture)
if not texture then
return
end
if type(texture.SetAtlas) == "function" then
local ok = pcall(texture.SetAtlas, texture, "roleicon-tiny-tank", true)
if ok then
return
end
end
texture:SetTexture(ROLE_ICON_TEXTURE)
if type(GetTexCoordsForRoleSmallCircle) == "function" then
local left, right, top, bottom = GetTexCoordsForRoleSmallCircle("TANK")
if left and right and top and bottom then
texture:SetTexCoord(left, right, top, bottom)
return
end
elseif type(GetTexCoordsForRole) == "function" then
local left, right, top, bottom = GetTexCoordsForRole("TANK")
if left and right and top and bottom then
texture:SetTexCoord(left, right, top, bottom)
return
end
end
texture:SetTexCoord(0, 0.26171875, 0.26171875, 0.5234375)
end
local function HasAssignments(slots)
if type(slots) ~= "table" then
return false
end
for slot = 1, 5 do
if NormalizeRuneKey(slots[slot]) ~= "" then
return true
end
end
return false
end
local function UnitCanSendSequence(unitId)
if not unitId or not UnitExists(unitId) then
return false
end
if UnitIsGroupLeader and UnitIsGroupLeader(unitId) then
return true
end
if UnitIsGroupAssistant and UnitIsGroupAssistant(unitId) then
return true
end
return false
end
function LR:IsSequenceSenderAllowed(playerName)
if playerName and playerName ~= "" then
local unitId = HMGT and HMGT.GetUnitForPlayer and HMGT:GetUnitForPlayer(playerName) or nil
return UnitCanSendSequence(unitId)
end
if not IsInGroup() and not IsInRaid() then
return true
end
return UnitCanSendSequence("player")
end
function LR:CanBroadcastSequence()
return self:IsSequenceSenderAllowed(nil)
end
function LR:IsTestMode()
return self.testMode == true
end
function LR:CanUseRuneInput()
return self:IsTestMode() or self:CanBroadcastSequence()
end
function LR:IsLuraEncounter(encounterId, encounterName)
local id = tonumber(encounterId) or 0
if LURA_ENCOUNTER_IDS[id] then
return true
end
return TextLooksLikeLura(encounterName)
end
function LR:HasLuraUnit()
for _, unitId in ipairs(LURA_SCAN_UNITS) do
if UnitLooksLikeLura(unitId) then
return true
end
end
return false
end
function LR:IsInLuraContext()
if self.luraEncounterActive == true then
return true
end
local inInstance, instanceType = IsInInstance()
if inInstance ~= true or instanceType ~= "raid" then
return false
end
local mapId = C_Map and C_Map.GetBestMapForUnit and C_Map.GetBestMapForUnit("player") or nil
if mapId and LURA_CONTEXT_MAP_IDS[mapId] then
return true
end
local mapInfo = mapId and C_Map and C_Map.GetMapInfo and C_Map.GetMapInfo(mapId) or nil
if mapInfo and TextLooksLikeLura(mapInfo.name) then
return true
end
if TextLooksLikeLura(GetSubZoneText and GetSubZoneText() or nil)
or TextLooksLikeLura(GetMinimapZoneText and GetMinimapZoneText() or nil)
or TextLooksLikeLura(GetRealZoneText and GetRealZoneText() or nil) then
return true
end
return self:HasLuraUnit()
end
function LR:ShouldAutoShowActionBar()
local settings = self:GetSettings()
return settings.actionBar.autoShow == true
and self:CanBroadcastSequence()
and self:IsInLuraContext()
end
function LR:RefreshContext(reason)
local contextActive = self:IsInLuraContext()
local canUse = self:CanUseRuneInput()
local autoShow = self:ShouldAutoShowActionBar()
if contextActive ~= self.lastContextActive or canUse ~= self.lastCanUse or autoShow ~= self.lastAutoShow then
Debug(
"verbose",
"Lura context reason=%s active=%s autoShow=%s canUse=%s",
tostring(reason or "refresh"),
tostring(contextActive),
tostring(autoShow),
tostring(canUse)
)
self.lastContextActive = contextActive
self.lastCanUse = canUse
self.lastAutoShow = autoShow
end
self:RefreshActionBar()
end
function LR:OnEncounterStart(encounterId, encounterName)
self.luraEncounterActive = self:IsLuraEncounter(encounterId, encounterName)
if self.luraEncounterActive then
Debug("info", "Lura encounter context started encounter=%s", tostring(encounterId or "?"))
end
self:RefreshContext("encounter_start")
end
function LR:OnEncounterEnd(encounterId)
if self.luraEncounterActive then
Debug("info", "Lura encounter context ended encounter=%s", tostring(encounterId or "?"))
end
self.luraEncounterActive = false
self:RefreshContext("encounter_end")
end
function LR:LogNotLeader(context)
Debug("info", "Lura rune %s blocked: only raid leader or raid assist can send", tostring(context or "sequence"))
end
local function SplitAssignments(payload)
local slots = {}
local text = tostring(payload or "")
local index = 1
for token in string.gmatch(text .. ",", "([^,]*),") do
if index > 5 then
break
end
slots[index] = NormalizeRuneKey(token)
index = index + 1
end
for slot = 1, 5 do
slots[slot] = NormalizeRuneKey(slots[slot])
end
return slots
end
function LR:GetSettings()
local settings = EA:GetLuraRunesSettings()
settings.enabled = settings.enabled == true
settings.unlocked = settings.unlocked == true
settings.posX = math.floor(ClampNumber(settings.posX, -1200, 1200, 0) + 0.5)
settings.posY = math.floor(ClampNumber(settings.posY, -900, 900, -120) + 0.5)
settings.iconSize = math.floor(ClampNumber(settings.iconSize, 28, 80, 44) + 0.5)
settings.backgroundAlpha = ClampNumber(settings.backgroundAlpha, 0, 0.8, 0.14)
settings.showLabels = settings.showLabels ~= false
settings.actionBar = type(settings.actionBar) == "table" and settings.actionBar or {}
settings.actionBar.shown = settings.actionBar.shown == true
settings.actionBar.autoShow = settings.actionBar.autoShow ~= false
settings.actionBar.unlocked = settings.actionBar.unlocked == true
settings.actionBar.posX = math.floor(ClampNumber(settings.actionBar.posX, -1200, 1200, 0) + 0.5)
settings.actionBar.posY = math.floor(ClampNumber(settings.actionBar.posY, -900, 900, -300) + 0.5)
settings.actionBar.iconSize = math.floor(ClampNumber(settings.actionBar.iconSize, 28, 80, 42) + 0.5)
settings.actionBar.iconSpacing = math.floor(ClampNumber(settings.actionBar.iconSpacing, 0, 80, 8) + 0.5)
settings.actionBar.orientation = NormalizeActionBarOrientation(settings.actionBar.orientation)
settings.actionBar.border = type(settings.actionBar.border) == "table" and settings.actionBar.border or {}
settings.actionBar.border.enabled = settings.actionBar.border.enabled == true
settings.actionBar.border.width = math.floor(ClampNumber(settings.actionBar.border.width, 1, 12, 2) + 0.5)
settings.actionBar.border.color = NormalizeColor(settings.actionBar.border.color, { r = 1, g = 0.82, b = 0.1, a = 0.9 })
settings.slots = type(settings.slots) == "table" and settings.slots or {}
for slot = 1, 5 do
settings.slots[slot] = NormalizeRuneKey(settings.slots[slot])
end
return settings
end
function LR:GetRuneLabel(key)
local normalized = NormalizeRuneKey(key)
local data = RUNE_DATA[normalized]
if data then
return data.label
end
return L["OPT_EA_LURA_RUNE_EMPTY"] or "Empty"
end
function LR:GetRuneTexture(key)
local normalized = NormalizeRuneKey(key)
local data = RUNE_DATA[normalized]
return (data and data.texture) or nil
end
function LR:GetRuneChatToken(key)
local normalized = NormalizeRuneKey(key)
local data = RUNE_DATA[normalized]
return (data and data.chatToken) or normalized
end
function LR:GetAssignmentsSummary()
local settings = self:GetSettings()
local parts = {}
for slot = 1, 5 do
parts[#parts + 1] = string.format("%d=%s", slot, self:GetRuneLabel(settings.slots[slot]))
end
return table.concat(parts, ", ")
end
function LR:SerializeAssignments()
local settings = self:GetSettings()
local parts = {}
for slot = 1, 5 do
parts[slot] = NormalizeRuneKey(settings.slots[slot])
end
return table.concat(parts, ",")
end
function LR:SavePosition()
local frame = self.frame
if not frame then
return
end
local frameCenterX, frameCenterY = frame:GetCenter()
local parentCenterX, parentCenterY = UIParent:GetCenter()
if not frameCenterX or not frameCenterY or not parentCenterX or not parentCenterY then
return
end
local settings = self:GetSettings()
settings.posX = math.floor(frameCenterX - parentCenterX + 0.5)
settings.posY = math.floor(frameCenterY - parentCenterY + 0.5)
end
function LR:SaveActionBarPosition()
local frame = self.actionBarFrame
if not frame then
return
end
local frameCenterX, frameCenterY = frame:GetCenter()
local parentCenterX, parentCenterY = UIParent:GetCenter()
if not frameCenterX or not frameCenterY or not parentCenterX or not parentCenterY then
return
end
local settings = self:GetSettings()
settings.actionBar.posX = math.floor(frameCenterX - parentCenterX + 0.5)
settings.actionBar.posY = math.floor(frameCenterY - parentCenterY + 0.5)
end
function LR:ApplyFrameStyle()
local frame = self.frame
if not frame then
return
end
local settings = self:GetSettings()
frame:SetScale(1)
frame:ClearAllPoints()
frame:SetPoint("CENTER", UIParent, "CENTER", settings.posX or 0, settings.posY or -120)
frame:EnableMouse(settings.unlocked == true)
if type(frame.SetBackdrop) == "function" then
local backgroundAlpha = settings.backgroundAlpha or 0.14
if settings.unlocked == true then
frame:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 },
})
frame:SetBackdropColor(0, 0, 0, backgroundAlpha)
frame:SetBackdropBorderColor(0, 0, 0, 1)
elseif backgroundAlpha > 0 then
frame:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
})
frame:SetBackdropColor(0, 0, 0, backgroundAlpha)
else
frame:SetBackdrop(nil)
end
end
if frame.dragHint then
frame.dragHint:SetShown(settings.unlocked == true)
end
local iconSize = settings.iconSize or 44
local iconSpacing = 8
local slotSize = iconSize
local radiusX = math.max(116, (iconSize + iconSpacing) * 2.35)
local radiusY = math.max(104, (iconSize + iconSpacing) * 2.15)
frame:SetSize(math.max(340, radiusX * 2.25 + slotSize), math.max(300, radiusY * 2.1 + slotSize))
local positions = {
[1] = { -radiusX * 0.56, -radiusY * 0.70 },
[2] = { -radiusX, radiusY * 0.08 },
[3] = { 0, radiusY },
[4] = { radiusX, radiusY * 0.08 },
[5] = { radiusX * 0.56, -radiusY * 0.70 },
}
for slot = 1, 5 do
local slotFrame = frame.slots and frame.slots[slot]
if slotFrame then
slotFrame:ClearAllPoints()
slotFrame:SetPoint("CENTER", frame, "CENTER", positions[slot][1], positions[slot][2])
slotFrame:SetSize(slotSize, slotSize)
slotFrame.icon:SetSize(iconSize, iconSize)
if type(slotFrame.SetBackdrop) == "function" then
slotFrame:SetBackdrop(nil)
end
slotFrame.label:SetShown(false)
end
end
if frame.tank then
frame.tank:ClearAllPoints()
frame.tank:SetPoint("CENTER", frame, "CENTER", 0, -radiusY * 0.98)
frame.tank:SetSize(math.max(28, iconSize * 0.72), math.max(28, iconSize * 0.72))
frame.tank.icon:SetAllPoints(frame.tank)
end
end
function LR:UpdateSlot(slot)
local frame = self.frame
local slotFrame = frame and frame.slots and frame.slots[slot]
if not slotFrame then
return
end
local settings = self:GetSettings()
local key = NormalizeRuneKey(settings.slots[slot])
local texture = self:GetRuneTexture(key)
if texture then
slotFrame.icon:SetTexture(texture)
slotFrame.icon:SetVertexColor(1, 1, 1, 1)
else
slotFrame.icon:SetTexture(FALLBACK_TEXTURE)
slotFrame.icon:SetVertexColor(0.18, 0.18, 0.18, 0.45)
end
slotFrame.label:SetText("")
end
function LR:UpdateFrame()
if not self.frame then
return
end
self:ApplyFrameStyle()
for slot = 1, 5 do
self:UpdateSlot(slot)
end
end
function LR:ApplyActionBarStyle()
local frame = self.actionBarFrame
if not frame then
return
end
local settings = self:GetSettings()
local actionBar = settings.actionBar
local iconSpacing = actionBar.iconSpacing or 8
local borderWidth = actionBar.border and actionBar.border.enabled and (actionBar.border.width or 2) or 0
frame:SetScale(1)
frame:ClearAllPoints()
frame:SetPoint("CENTER", UIParent, "CENTER", actionBar.posX or 0, actionBar.posY or -300)
frame:EnableMouse(actionBar.unlocked == true)
if type(frame.SetBackdrop) == "function" then
if actionBar.unlocked == true then
frame:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 },
})
frame:SetBackdropColor(0, 0, 0, 0.28)
frame:SetBackdropBorderColor(0, 0, 0, 1)
else
frame:SetBackdrop(nil)
end
end
if frame.dragHint then
frame.dragHint:SetShown(actionBar.unlocked == true)
end
local buttons = frame.buttons or {}
local iconSize = actionBar.iconSize or 42
local buttonSize = iconSize + (borderWidth * 2)
local spacing = iconSpacing
local padding = 8
local count = math.max(1, #buttons)
local orientation = NormalizeActionBarOrientation(actionBar.orientation)
if orientation == "vertical" then
frame:SetSize(buttonSize + padding, (count * buttonSize) + ((count - 1) * spacing) + padding)
else
frame:SetSize((count * buttonSize) + ((count - 1) * spacing) + padding, buttonSize + padding)
end
local totalLength = (count * buttonSize) + ((count - 1) * spacing)
local start = -(totalLength - buttonSize) / 2
for index, button in ipairs(buttons) do
button:SetSize(buttonSize, buttonSize)
if button.icon then
button.icon:SetSize(iconSize, iconSize)
end
ApplyIconBorder(button, actionBar, button.isClear and { r = 0.18, g = 0, b = 0, a = 0.48 } or nil)
button:ClearAllPoints()
if orientation == "vertical" then
button:SetPoint("CENTER", frame, "CENTER", 0, -start - ((index - 1) * (buttonSize + spacing)))
else
button:SetPoint("CENTER", frame, "CENTER", start + ((index - 1) * (buttonSize + spacing)), 0)
end
end
local canUse = self:CanUseRuneInput()
for _, button in ipairs(buttons) do
if type(button.SetEnabled) == "function" then
button:SetEnabled(canUse)
end
button:SetAlpha(canUse and 1 or 0.45)
end
end
function LR:ShouldShowActionBar()
local settings = self:GetSettings()
return EA.runtimeEnabled == true
and settings.enabled == true
and (settings.actionBar.shown == true or self:IsTestMode() or self:ShouldAutoShowActionBar())
end
function LR:RefreshActionBar()
local frame = self.actionBarFrame
if not frame and not self:ShouldShowActionBar() then
return
end
frame = self:EnsureActionBar()
self:ApplyActionBarStyle()
if self:ShouldShowActionBar() then
frame:Show()
else
frame:Hide()
end
end
function LR:EnsureActionBar()
if self.actionBarFrame then
return self.actionBarFrame
end
local frame = CreateFrame("Frame", "HMGT_LuraRuneActionBar", UIParent, "BackdropTemplate")
frame:SetSize(316, 58)
frame:SetFrameStrata("FULLSCREEN_DIALOG")
frame:SetFrameLevel(205)
frame:SetClampedToScreen(true)
frame:SetMovable(true)
frame:RegisterForDrag("LeftButton")
frame:SetScript("OnDragStart", function(selfFrame)
if LR:GetSettings().actionBar.unlocked == true then
selfFrame:StartMoving()
end
end)
frame:SetScript("OnDragStop", function(selfFrame)
selfFrame:StopMovingOrSizing()
LR:SaveActionBarPosition()
LR:ApplyActionBarStyle()
end)
frame:Hide()
local dragHint = frame:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
dragHint:SetPoint("TOP", frame, "BOTTOM", 0, -2)
dragHint:SetText(L["OPT_EA_LURA_DRAG_HINT"] or "Drag to move")
frame.dragHint = dragHint
frame.buttons = {}
local size = 42
for _, key in ipairs(RUNE_ORDER) do
local data = RUNE_DATA[key] or {}
local button = CreateFrame("Button", nil, frame, "BackdropTemplate")
button:SetSize(size, size)
button.icon = button:CreateTexture(nil, "ARTWORK")
button.icon:SetPoint("CENTER")
button.icon:SetSize(size - 8, size - 8)
button.icon:SetTexture(data.texture or FALLBACK_TEXTURE)
button.icon:SetVertexColor(1, 1, 1, 1)
button:SetScript("OnClick", function()
LR:AppendRuneToSequence(key)
LR:ApplyActionBarStyle()
end)
frame.buttons[#frame.buttons + 1] = button
end
local clearButton = CreateFrame("Button", nil, frame, "BackdropTemplate")
clearButton:SetSize(size, size)
clearButton.isClear = true
clearButton.icon = clearButton:CreateTexture(nil, "ARTWORK")
clearButton.icon:SetPoint("CENTER")
clearButton.icon:SetSize(size - 8, size - 8)
clearButton.icon:SetTexture(CLEAR_BUTTON_TEXTURE)
clearButton.icon:SetVertexColor(1, 1, 1, 1)
clearButton:SetScript("OnClick", function()
LR:ClearAssignments(false)
LR:ApplyActionBarStyle()
end)
frame.clearButton = clearButton
frame.buttons[#frame.buttons + 1] = clearButton
self.actionBarFrame = frame
self:ApplyActionBarStyle()
return frame
end
function LR:EnsureFrame()
if self.frame then
return self.frame
end
local frame = CreateFrame("Frame", "HMGT_LuraRunesFrame", UIParent, "BackdropTemplate")
frame:SetSize(340, 300)
frame:SetFrameStrata("FULLSCREEN_DIALOG")
frame:SetFrameLevel(190)
frame:SetClampedToScreen(true)
frame:SetMovable(true)
frame:RegisterForDrag("LeftButton")
frame:SetScript("OnDragStart", function(selfFrame)
if LR:GetSettings().unlocked == true then
selfFrame:StartMoving()
end
end)
frame:SetScript("OnDragStop", function(selfFrame)
selfFrame:StopMovingOrSizing()
LR:SavePosition()
LR:ApplyFrameStyle()
end)
frame:Hide()
local dragHint = frame:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
dragHint:SetPoint("TOP", frame, "TOP", 0, -4)
dragHint:SetText(L["OPT_EA_LURA_DRAG_HINT"] or "Drag to move")
frame.dragHint = dragHint
local boss = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
boss:SetPoint("CENTER", frame, "CENTER", 0, 0)
boss:SetText(L["OPT_EA_LURA_BOSS"] or "Boss")
frame.boss = boss
local tank = CreateFrame("Frame", nil, frame)
tank:SetSize(32, 32)
tank.icon = tank:CreateTexture(nil, "ARTWORK")
tank.icon:SetAllPoints(tank)
SetTankIconTexture(tank.icon)
frame.tank = tank
frame.slots = {}
for slot = 1, 5 do
local slotFrame = CreateFrame("Frame", nil, frame, "BackdropTemplate")
slotFrame.icon = slotFrame:CreateTexture(nil, "ARTWORK")
slotFrame.icon:SetPoint("CENTER", slotFrame, "CENTER", 0, 0)
slotFrame.icon:SetTexture(FALLBACK_TEXTURE)
slotFrame.number = slotFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
slotFrame.number:SetPoint("TOPLEFT", slotFrame, "TOPLEFT", 5, -3)
slotFrame.number:SetText(tostring(slot))
slotFrame.label = slotFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
slotFrame.label:SetPoint("TOP", slotFrame.icon, "BOTTOM", 0, -2)
slotFrame.label:SetWidth(90)
slotFrame.label:SetJustifyH("CENTER")
slotFrame.label:SetText("")
slotFrame.label:Hide()
frame.slots[slot] = slotFrame
end
self.frame = frame
self:UpdateFrame()
return frame
end
function LR:ShouldShow()
local settings = self:GetSettings()
return EA.runtimeEnabled == true
and settings.enabled == true
and (settings.unlocked == true or HasAssignments(settings.slots))
end
function LR:Refresh()
local frame = self:EnsureFrame()
self:UpdateFrame()
if self:ShouldShow() then
frame:Show()
else
frame:Hide()
end
self:RefreshActionBar()
end
function LR:Show()
local settings = self:GetSettings()
EA:GetSettings().enabled = true
EA.runtimeEnabled = true
settings.enabled = true
self:EnsureFrame()
self:UpdateFrame()
self.frame:Show()
NotifyOptionsChanged()
end
function LR:Hide()
self.testMode = false
if self.frame then
self.frame:Hide()
end
if self.actionBarFrame then
self.actionBarFrame:Hide()
end
end
function LR:ApplyAssignments(slots, source)
local settings = self:GetSettings()
for slot = 1, 5 do
settings.slots[slot] = NormalizeRuneKey(slots and slots[slot])
end
self:Refresh()
NotifyOptionsChanged()
Debug("info", "Lura runes updated source=%s %s", tostring(source or "local"), self:GetAssignmentsSummary())
end
function LR:ApplyTestPattern()
self.testMode = true
Debug("info", "Lura local test mode enabled")
self:ApplyAssignments(DEFAULT_TEST_ASSIGNMENTS, "test")
end
function LR:ClearAssignments(broadcast)
self:ApplyAssignments({}, "clear")
if broadcast then
self:BroadcastAssignments()
end
end
function LR:GetNextEmptySequenceSlot()
local settings = self:GetSettings()
for slot = 1, 5 do
if NormalizeRuneKey(settings.slots[slot]) == "" then
return slot
end
end
return nil
end
function LR:LogSequenceProgress(slot, key)
local label = self:GetRuneLabel(key)
if slot >= 5 then
Debug("info", "Lura rune sequence complete %s", self:GetAssignmentsSummary())
else
Debug("info", "Lura rune %s saved as slot %d; waiting for next rune", tostring(label or "?"), slot)
end
end
function LR:SendRuneRaidChat(slot, key)
if self:IsTestMode() or not self:CanBroadcastSequence() then
return false
end
if not IsInRaid() then
Debug("verbose", "Lura raid chat skipped: player is not in raid")
return false
end
local slotIndex = tonumber(slot) or 0
if slotIndex < 1 or slotIndex > 5 then
return false
end
local token = self:GetRuneChatToken(key)
if not token or token == "" then
return false
end
local message = string.format("HMGT:Rune%d:%s", slotIndex, token)
local ok = false
if C_ChatInfo and type(C_ChatInfo.SendChatMessage) == "function" then
ok = pcall(C_ChatInfo.SendChatMessage, message, "RAID")
elseif type(SendChatMessage) == "function" then
ok = pcall(SendChatMessage, message, "RAID")
end
if ok then
Debug("info", "Lura raid chat sent %s", message)
return true
end
Debug("error", "Lura raid chat failed %s", message)
return false
end
function LR:SendRuneRaidChatSequence()
local settings = self:GetSettings()
local sent = 0
for slot = 1, 5 do
local key = NormalizeRuneKey(settings.slots[slot])
if key ~= "" and self:SendRuneRaidChat(slot, key) then
sent = sent + 1
end
end
return sent
end
function LR:AppendRuneToSequence(key)
local runeKey = NormalizeRuneKey(key)
if runeKey == "" then
Debug("info", "Lura rune input ignored: unknown rune. Valid runes: circle, x, diamond, t, triangle")
return false
end
if not self:CanUseRuneInput() then
self:LogNotLeader("input")
return false
end
local settings = self:GetSettings()
local slot = self:GetNextEmptySequenceSlot()
if not slot then
for index = 1, 5 do
settings.slots[index] = ""
end
slot = 1
end
EA:GetSettings().enabled = true
EA.runtimeEnabled = true
settings.enabled = true
settings.slots[slot] = runeKey
self:Refresh()
NotifyOptionsChanged()
self:LogSequenceProgress(slot, runeKey)
Debug("info", "Lura rune input slot=%d rune=%s %s", slot, tostring(runeKey), self:GetAssignmentsSummary())
self:SendRuneRaidChat(slot, runeKey)
if slot >= 5 and self:CanBroadcastSequence() then
self:BroadcastAssignments(false)
elseif slot >= 5 then
Debug("info", "Lura local test sequence complete; not sending to raid")
end
return true
end
function LR:BroadcastAssignments(sendRaidChat)
if not self:CanBroadcastSequence() then
self:LogNotLeader("sequence send")
return false
end
local prefix = HMGT.MSG_LURA_RUNES or "LUR"
local payload = self:SerializeAssignments()
if sendRaidChat ~= false then
self:SendRuneRaidChatSequence()
end
HMGT:SendGroupMessage(string.format("%s|%s", prefix, payload), "ALERT")
Debug("info", "Lura rune sequence sent %s", self:GetAssignmentsSummary())
return true
end
function LR:HandleComm(senderName, payload)
local settings = self:GetSettings()
if EA.runtimeEnabled ~= true or settings.enabled ~= true then
return
end
if not self:IsSequenceSenderAllowed(senderName) then
Debug("info", "Lura rune sequence ignored from non-leader/non-assist sender=%s", tostring(senderName or "?"))
return
end
self:ApplyAssignments(SplitAssignments(payload), senderName)
end
function LR:HandleRaidChatMessage(message, senderName, event)
local slot, key = ParseRuneRaidChatMessage(message)
if not slot or not key then
return false
end
local settings = self:GetSettings()
if EA.runtimeEnabled ~= true or settings.enabled ~= true then
Debug("verbose", "Lura raid chat ignored while disabled event=%s sender=%s", tostring(event or "?"), tostring(senderName or "?"))
return false
end
if not self:IsSequenceSenderAllowed(senderName) then
Debug("info", "Lura raid chat ignored from non-leader/non-assist sender=%s", tostring(senderName or "?"))
return false
end
settings.slots[slot] = key
self:Refresh()
NotifyOptionsChanged()
Debug("info", "Lura raid chat applied sender=%s slot=%d rune=%s", tostring(senderName or "?"), slot, tostring(key))
return true
end
function LR:HandleSlashCommand(input)
local rest = tostring(input or ""):match("^lura%s*(.*)$") or ""
rest = rest:gsub("^%s+", ""):gsub("%s+$", "")
if rest == "" or rest == "show" then
self:Show()
return
end
if rest == "hide" then
self:Hide()
return
end
if rest == "unlock" then
local settings = self:GetSettings()
settings.unlocked = not settings.unlocked
self:Show()
self:Refresh()
return
end
if rest == "bar" or rest == "buttons" or rest == "actionbar" then
local settings = self:GetSettings()
EA:GetSettings().enabled = true
EA.runtimeEnabled = true
settings.enabled = true
settings.actionBar.shown = not settings.actionBar.shown
self:Refresh()
NotifyOptionsChanged()
return
end
if rest == "bar unlock" or rest == "buttons unlock" or rest == "actionbar unlock" then
local settings = self:GetSettings()
EA:GetSettings().enabled = true
EA.runtimeEnabled = true
settings.enabled = true
settings.actionBar.shown = true
settings.actionBar.unlocked = not settings.actionBar.unlocked
self:Refresh()
NotifyOptionsChanged()
return
end
if rest == "test" then
self:Show()
self:ApplyTestPattern()
return
end
if rest == "send" or rest == "broadcast" then
self:BroadcastAssignments()
return
end
if rest == "clear" then
self:ClearAssignments(true)
return
end
if rest == "reset" or rest == "new" then
self.testMode = false
self:ClearAssignments(false)
return
end
local slots = {}
local index = 1
for token in rest:gmatch("[^,%s]+") do
if index > 5 then
break
end
slots[index] = NormalizeRuneKey(token)
index = index + 1
end
if index == 2 then
self:AppendRuneToSequence(slots[1])
return
end
if index > 1 then
self:Show()
self:ApplyAssignments(slots, "slash")
self:BroadcastAssignments()
else
Debug("info", "Lura slash usage: /hmgt lura test | reset | clear | unlock | send | circle x diamond t triangle")
end
end