diff --git a/HailMaryGuildTools.lua b/HailMaryGuildTools.lua index e422c33..959d480 100644 --- a/HailMaryGuildTools.lua +++ b/HailMaryGuildTools.lua @@ -40,6 +40,7 @@ local MSG_SYNC_REQUEST = "SRQ" local MSG_SYNC_RESPONSE = "SRS" -- SRS|version|protocol|class|spec|talentHash|knownSpellIds|cd1:t1:d1;... local MSG_RAID_TIMELINE = "RTL" -- RTL|encounterId|time|spellId|leadTime|alertText local MSG_RAID_TIMELINE_TEST = "RTS" -- RTS|encounterId|difficultyId|serverStartTime|duration +local MSG_LURA_RUNES = "LUR" -- LUR|slot1,slot2,slot3,slot4,slot5 local MSG_RELIABLE = "REL" -- REL|messageId|innerPayload local MSG_ACK = "ACK" -- ACK|messageId local COMM_PREFIX = "HMGT" @@ -85,6 +86,7 @@ HMGT.MSG_RELIABLE = MSG_RELIABLE HMGT.MSG_ACK = MSG_ACK HMGT.MSG_RAID_TIMELINE = MSG_RAID_TIMELINE HMGT.MSG_RAID_TIMELINE_TEST = MSG_RAID_TIMELINE_TEST +HMGT.MSG_LURA_RUNES = MSG_LURA_RUNES -- ── Standardwerte ───────────────────────────────────────────── local defaults = { @@ -119,6 +121,34 @@ local defaults = { alertColor = { r = 1, g = 0.82, b = 0.15, a = 1 }, encounters = {}, }, + encounterAlerts = { + enabled = false, + luraRunes = { + enabled = false, + unlocked = false, + posX = 0, + posY = -120, + iconSize = 44, + backgroundAlpha = 0.14, + showLabels = true, + actionBar = { + shown = false, + autoShow = true, + unlocked = false, + posX = 0, + posY = -300, + iconSize = 42, + iconSpacing = 8, + orientation = "horizontal", + border = { + enabled = false, + width = 2, + color = { r = 1, g = 0.82, b = 0.1, a = 0.9 }, + }, + }, + slots = {}, + }, + }, notes = { enabled = true, mainText = "", @@ -196,6 +226,7 @@ local DEBUG_SCOPE_LABELS = { PowerSpend = "Power Spend", RaidTimeline = "Raid Timeline", Notes = "Notes", + EncounterAlerts = "Encounter Alerts", } local DEBUG_LEVELS = { error = 1, @@ -1458,6 +1489,67 @@ local function NormalizeRaidTimelineSettings(settings) settings.encounters = normalizedEncounters end +local VALID_LURA_RUNE_KEYS = { + circle = true, + cross = true, + diamond = true, + t = true, + triangle = true, +} + +local function NormalizeLuraRuneKey(value) + local key = tostring(value or ""):lower() + if VALID_LURA_RUNE_KEYS[key] then + return key + end + return "" +end + +local function NormalizeLuraRunesSettings(settings) + if type(settings) ~= "table" then return end + settings.enabled = settings.enabled == true + settings.unlocked = settings.unlocked == true + settings.posX = math.floor(NormalizeLayoutValue(settings.posX, -1200, 1200, 0) + 0.5) + settings.posY = math.floor(NormalizeLayoutValue(settings.posY, -900, 900, -120) + 0.5) + settings.iconSize = math.floor(NormalizeLayoutValue(settings.iconSize, 28, 80, 44) + 0.5) + settings.backgroundAlpha = NormalizeLayoutValue(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(NormalizeLayoutValue(settings.actionBar.posX, -1200, 1200, 0) + 0.5) + settings.actionBar.posY = math.floor(NormalizeLayoutValue(settings.actionBar.posY, -900, 900, -300) + 0.5) + settings.actionBar.iconSize = math.floor(NormalizeLayoutValue(settings.actionBar.iconSize, 28, 80, 42) + 0.5) + settings.actionBar.iconSpacing = math.floor(NormalizeLayoutValue(settings.actionBar.iconSpacing, 0, 80, 8) + 0.5) + 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 {} + settings.actionBar.border.enabled = settings.actionBar.border.enabled == true + settings.actionBar.border.width = math.floor(NormalizeLayoutValue(settings.actionBar.border.width, 1, 12, 2) + 0.5) + settings.actionBar.border.color = type(settings.actionBar.border.color) == "table" and settings.actionBar.border.color or {} + settings.actionBar.border.color.r = NormalizeLayoutValue(settings.actionBar.border.color.r, 0, 1, 1) + settings.actionBar.border.color.g = NormalizeLayoutValue(settings.actionBar.border.color.g, 0, 1, 0.82) + settings.actionBar.border.color.b = NormalizeLayoutValue(settings.actionBar.border.color.b, 0, 1, 0.1) + settings.actionBar.border.color.a = NormalizeLayoutValue(settings.actionBar.border.color.a, 0, 1, 0.9) + + local slots = type(settings.slots) == "table" and settings.slots or {} + local normalizedSlots = {} + for slot = 1, 5 do + normalizedSlots[slot] = NormalizeLuraRuneKey(slots[slot]) + end + settings.slots = normalizedSlots +end + +local function NormalizeEncounterAlertsSettings(settings) + if type(settings) ~= "table" then return end + settings.enabled = settings.enabled == true + settings.luraRunes = type(settings.luraRunes) == "table" and settings.luraRunes or {} + NormalizeLuraRunesSettings(settings.luraRunes) +end + local function NormalizeNotesSettings(settings) if type(settings) ~= "table" then return end settings.enabled = settings.enabled ~= false @@ -1762,6 +1854,8 @@ function HMGT:MigrateProfileSettings() p.personalAuras = nil p.raidTimeline = p.raidTimeline or {} NormalizeRaidTimelineSettings(p.raidTimeline) + p.encounterAlerts = p.encounterAlerts or {} + NormalizeEncounterAlertsSettings(p.encounterAlerts) p.notes = p.notes or {} NormalizeNotesSettings(p.notes) p.minimap = p.minimap or {} @@ -3029,6 +3123,7 @@ function HMGT:GetHealthStatusLines() AuraExpiry = self.AuraExpiry ~= nil, MapOverlay = self.MapOverlay ~= nil, RaidTimeline = self.RaidTimeline ~= nil, + EncounterAlerts = self.EncounterAlerts ~= nil, Notes = self.Notes ~= nil, } local moduleParts = {} @@ -3108,6 +3203,12 @@ function HMGT:SlashCommand(input) else self:OpenConfig() end + elseif input:find("^lura") == 1 then + if self.EncounterAlerts and self.EncounterAlerts.HandleSlashCommand then + self.EncounterAlerts:HandleSlashCommand(input) + else + self:OpenConfig() + end elseif input:find("^debugdump") == 1 then local n = tonumber(input:match("^debugdump%s+(%d+)$")) if self.DumpDevToolsLog then diff --git a/HailMaryGuildTools.toc b/HailMaryGuildTools.toc index bef1e5e..4da4cc6 100644 --- a/HailMaryGuildTools.toc +++ b/HailMaryGuildTools.toc @@ -66,3 +66,8 @@ Modules\RaidTimeline\RaidTimelineBossAbilityData.lua Modules\RaidTimeline\RaidTimeline.lua Modules\RaidTimeline\RaidTimelineBigWigs.lua Modules\RaidTimeline\RaidTimelineOptions.lua + +# EncounterAlerts +Modules\EncounterAlerts\EncounterAlerts.lua +Modules\EncounterAlerts\LuraRunes.lua +Modules\EncounterAlerts\EncounterAlertsOptions.lua diff --git a/HailMaryGuildToolsOptions.lua b/HailMaryGuildToolsOptions.lua index 948bbef..fec4454 100644 --- a/HailMaryGuildToolsOptions.lua +++ b/HailMaryGuildToolsOptions.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 @@ -1933,6 +1936,7 @@ function HMGT_Config:Initialize() "|cffffd100/hmgt|r", "|cffffd100/hmgt debug|r", "|cffffd100/hmgt status|r", + "|cffffd100/hmgt lura|r", "|cffffd100/hmgt version|r", }, "\n"), }, @@ -2083,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 @@ -2120,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") diff --git a/Locales/deDE.lua b/Locales/deDE.lua index 6a6d0a8..b369276 100644 --- a/Locales/deDE.lua +++ b/Locales/deDE.lua @@ -34,7 +34,7 @@ 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" @@ -75,6 +75,36 @@ 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" diff --git a/Locales/enUS.lua b/Locales/enUS.lua index cad8f90..f924893 100644 --- a/Locales/enUS.lua +++ b/Locales/enUS.lua @@ -34,7 +34,7 @@ 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" @@ -75,6 +75,36 @@ 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" diff --git a/Modules/EncounterAlerts/EncounterAlerts.lua b/Modules/EncounterAlerts/EncounterAlerts.lua new file mode 100644 index 0000000..cde529a --- /dev/null +++ b/Modules/EncounterAlerts/EncounterAlerts.lua @@ -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 diff --git a/Modules/EncounterAlerts/EncounterAlertsOptions.lua b/Modules/EncounterAlerts/EncounterAlertsOptions.lua new file mode 100644 index 0000000..8c9f480 --- /dev/null +++ b/Modules/EncounterAlerts/EncounterAlertsOptions.lua @@ -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) diff --git a/Modules/EncounterAlerts/LuraRunes.lua b/Modules/EncounterAlerts/LuraRunes.lua new file mode 100644 index 0000000..27f2100 --- /dev/null +++ b/Modules/EncounterAlerts/LuraRunes.lua @@ -0,0 +1,1113 @@ +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 diff --git a/Modules/EncounterAlerts/Media/LuraRunes/.gitkeep b/Modules/EncounterAlerts/Media/LuraRunes/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Modules/EncounterAlerts/Media/LuraRunes/.gitkeep @@ -0,0 +1 @@ + diff --git a/Modules/EncounterAlerts/Media/LuraRunes/Rune_Circle.tga b/Modules/EncounterAlerts/Media/LuraRunes/Rune_Circle.tga new file mode 100644 index 0000000..5610e4c Binary files /dev/null and b/Modules/EncounterAlerts/Media/LuraRunes/Rune_Circle.tga differ diff --git a/Modules/EncounterAlerts/Media/LuraRunes/Rune_Diamond.tga b/Modules/EncounterAlerts/Media/LuraRunes/Rune_Diamond.tga new file mode 100644 index 0000000..87e0be5 Binary files /dev/null and b/Modules/EncounterAlerts/Media/LuraRunes/Rune_Diamond.tga differ diff --git a/Modules/EncounterAlerts/Media/LuraRunes/Rune_T.tga b/Modules/EncounterAlerts/Media/LuraRunes/Rune_T.tga new file mode 100644 index 0000000..47382b3 Binary files /dev/null and b/Modules/EncounterAlerts/Media/LuraRunes/Rune_T.tga differ diff --git a/Modules/EncounterAlerts/Media/LuraRunes/Rune_Triangle.tga b/Modules/EncounterAlerts/Media/LuraRunes/Rune_Triangle.tga new file mode 100644 index 0000000..6773d79 Binary files /dev/null and b/Modules/EncounterAlerts/Media/LuraRunes/Rune_Triangle.tga differ diff --git a/Modules/EncounterAlerts/Media/LuraRunes/Rune_X.tga b/Modules/EncounterAlerts/Media/LuraRunes/Rune_X.tga new file mode 100644 index 0000000..20f3537 Binary files /dev/null and b/Modules/EncounterAlerts/Media/LuraRunes/Rune_X.tga differ diff --git a/Modules/Tracker/TrackerSync.lua b/Modules/Tracker/TrackerSync.lua index 7f7c7c5..370a9eb 100644 --- a/Modules/Tracker/TrackerSync.lua +++ b/Modules/Tracker/TrackerSync.lua @@ -1037,5 +1037,10 @@ function HMGT:OnCommReceived(prefix, message, distribution, sender) tonumber(duration) ) end + elseif msgType == HMGT.MSG_LURA_RUNES then + local payload = message:match("^%a+|(.+)$") or "" + if HMGT.EncounterAlerts and HMGT.EncounterAlerts.HandleLuraRunesComm then + HMGT.EncounterAlerts:HandleLuraRunesComm(senderName, payload) + end end end diff --git a/readme.md b/readme.md index 538809b..c9d7375 100644 --- a/readme.md +++ b/readme.md @@ -58,14 +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