1114 lines
35 KiB
Lua
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
|