diff --git a/.gitignore b/.gitignore index c9cf77a8..b611af61 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ EllesmereUIUnitFrames/Libs/ # Kiro workspace files .kiro/ .DS_Store +.vscode/ # Claude Code workspace files .claude/ diff --git a/EllesmereUIQoL/EUI_QoL_Options.lua b/EllesmereUIQoL/EUI_QoL_Options.lua index d9cf5586..8b45ca7a 100644 --- a/EllesmereUIQoL/EUI_QoL_Options.lua +++ b/EllesmereUIQoL/EUI_QoL_Options.lua @@ -237,6 +237,19 @@ initFrame:SetScript("OnEvent", function(self) end } ); y = y - h + _, h = W:DualRow(parent, y, + { type="toggle", text="Mythic Keystone Reminder", + tooltip="After a Mythic+ dungeon completion, remind you to swap to a swappable key in your bags.", + getValue=function() + return not (EllesmereUIDB and EllesmereUIDB.mythicKeyReminder == false) + end, + setValue=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.mythicKeyReminder = v + end }, + { type="label", text="" } + ); y = y - h + _, h = W:Spacer(parent, y, 20); y = y - h --------------------------------------------------------------------------- @@ -1072,7 +1085,26 @@ initFrame:SetScript("OnEvent", function(self) end } ); y = y - h - _, h = W:Spacer(parent, y, 20); y = y - h + -- Module toggle for the forked LFGMythicLocation feature. + -- When disabled, the addon stops listening for LFG invite updates. + _, h = W:DualRow(parent, y, + { type="toggle", text="Mythic LFG Location", + tooltip="Enable or disable the automatic Mythic LFG invite summary when an LFG invite is accepted.", + getValue=function() + if not EllesmereUIDB then return true end + return EllesmereUIDB.lfgMythicLocation ~= false + end, + setValue=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.lfgMythicLocation = v + if _G.EUI_LFGMythicLocation_UpdateState then + _G.EUI_LFGMythicLocation_UpdateState() + end + end }, + { type="label", text=" " } + ); y = y - h + + _, h = W:Spacer(parent, y, 44); y = y - h --------------------------------------------------------------------------- -- UI @@ -1167,6 +1199,7 @@ initFrame:SetScript("OnEvent", function(self) EllesmereUIDB.instanceResetAnnounceMsg = "" EllesmereUIDB.quickSignup = false EllesmereUIDB.persistSignupNote = false + EllesmereUIDB.lfgMythicLocation = true EllesmereUIDB.ahCurrentExpansion = false EllesmereUIDB.healthMacroEnabled = false EllesmereUIDB.healthMacroPrio1 = 1 diff --git a/EllesmereUIQoL/EllesmereUIQoL.lua b/EllesmereUIQoL/EllesmereUIQoL.lua index 7a6181bb..16706c4f 100644 --- a/EllesmereUIQoL/EllesmereUIQoL.lua +++ b/EllesmereUIQoL/EllesmereUIQoL.lua @@ -299,6 +299,252 @@ qolFrame:SetScript("OnEvent", function(self) end) end + --------------------------------------------------------------------------- + -- Mythic Keystone Swap Reminder + --------------------------------------------------------------------------- + do + local reminderActive = false + local reminderCompleted = false + local reminderShown = false + local reminderReadyToShow = false + local reminderMapID = nil + local reminderLevel = nil + + local function IsEnabled() + return not (EllesmereUIDB and EllesmereUIDB.mythicKeyReminder == false) + end + + local function IsDungeonComplete() + local numCriteria = select(3, C_Scenario.GetStepInfo()) or 0 + if numCriteria == 0 then return false end + + local seenAny = false + for i = 1, numCriteria do + local info = C_ScenarioInfo.GetCriteriaInfo(i) + if info then + seenAny = true + if not info.completed then + return false + end + end + end + + return seenAny + end + + local function _isInChallengeMode() + if C_ChallengeMode and C_ChallengeMode.IsChallengeModeActive + and C_ChallengeMode.IsChallengeModeActive() then + return true + end + local _, instanceType, difficulty = GetInstanceInfo() + return instanceType == "party" and difficulty == 8 + end + + local function CanSwapKeyInBags(mapID, runLevel) + if not mapID or not runLevel then return false end + local function IsDebugEnabled() + return (_G.EUI_MythicReminderDebug == true) or (EllesmereUIDB and EllesmereUIDB.mythicKeyReminderDebug) + end + local foundAnyKeystone = false + local foundMatchingKeystone = false + for bag = BACKPACK_CONTAINER, NUM_BAG_SLOTS do + local slots = C_Container.GetContainerNumSlots(bag) + for slot = 1, slots do + local link = C_Container.GetContainerItemLink(bag, slot) + if link and link:find("|Hkeystone:") then + foundAnyKeystone = true + local ksMap, ksLevel = link:match("keystone:(%d+):(%d+)") + ksMap = tonumber(ksMap) + ksLevel = tonumber(ksLevel) + if ksMap and ksLevel then + if IsDebugEnabled() then + print(string.format("[MythicReminder][debug] found keystone in bag: link=%s map=%s level=%s", tostring(link), tostring(ksMap), tostring(ksLevel))) + end + if ksMap == mapID then + if runLevel >= ksLevel then + return true + end + if IsDebugEnabled() then + print(string.format("[MythicReminder][debug] keystone matches map but is too high level: runLevel=%s keyLevel=%s", tostring(runLevel), tostring(ksLevel))) + end + else + if IsDebugEnabled() then + print(string.format("[MythicReminder][debug] keystone map mismatch: expected=%s got=%s", tostring(mapID), tostring(ksMap))) + end + end + elseif IsDebugEnabled() then + print("[MythicReminder][debug] failed to parse keystone link: " .. tostring(link)) + end + end + end + end + if IsDebugEnabled() then + if not foundAnyKeystone then + print("[MythicReminder][debug] no keystone found in bags") + else + print("[MythicReminder][debug] no swappable keystone found in bags for map=" .. tostring(mapID) .. " runLevel=" .. tostring(runLevel)) + end + end + return false + end + + local function ShowReminder(force) + if reminderShown or not IsEnabled() then return false end + if not reminderMapID or not reminderLevel then return false end + if not force and not CanSwapKeyInBags(reminderMapID, reminderLevel) then + if (_G.EUI_MythicReminderDebug == true) or (EllesmereUIDB and EllesmereUIDB.mythicKeyReminderDebug) then + print("[MythicReminder][debug] ShowReminder blocked: no swappable keystone for map=" .. tostring(reminderMapID) .. " level=" .. tostring(reminderLevel)) + end + return false + end + + reminderShown = true + local msg = "|cff00ff00[Mythic Reminder]|r Did you change your key? You have a swappable key for this dungeon in your bags." + print(msg) + if UIErrorsFrame and UIErrorsFrame.AddMessage then + UIErrorsFrame:AddMessage("Mythic Reminder: Did you change your key?", 0.0, 1.0, 0.0, 1.0) + end + PlaySound(8960, "Master") + if (_G.EUI_MythicReminderDebug == true) or (EllesmereUIDB and EllesmereUIDB.mythicKeyReminderDebug) then + print("[MythicReminder][debug] ShowReminder shown for map=" .. tostring(reminderMapID) .. " level=" .. tostring(reminderLevel)) + end + return true + end + + local function ResetReminder() + reminderActive = false + reminderCompleted = false + reminderReadyToShow = false + reminderShown = false + reminderMapID = nil + reminderLevel = nil + end + + local function StartReminder() + if not C_ChallengeMode or not C_ChallengeMode.GetActiveChallengeMapID + or not C_ChallengeMode.GetActiveKeystoneInfo then + return + end + local mapID = C_ChallengeMode.GetActiveChallengeMapID() + local level = select(1, C_ChallengeMode.GetActiveKeystoneInfo()) + if not mapID or not level then return end + reminderActive = true + reminderCompleted = false + reminderReadyToShow = false + reminderShown = false + reminderMapID = mapID + reminderLevel = level + -- Debug: log start state for troubleshooting + if EllesmereUI and EllesmereUI.Debug then + EllesmereUI:Debug("MythicReminder Start: mapID=" .. tostring(mapID) .. " level=" .. tostring(level)) + else + print("[MythicReminder] Start: mapID=" .. tostring(mapID) .. " level=" .. tostring(level)) + end + -- If debug is enabled, dump keystones currently in bags for troubleshooting + if (_G.EUI_MythicReminderDebug == true) or (EllesmereUIDB and EllesmereUIDB.mythicKeyReminderDebug) then + for bag = BACKPACK_CONTAINER, NUM_BAG_SLOTS do + local slots = C_Container.GetContainerNumSlots(bag) + for slot = 1, slots do + local link = C_Container.GetContainerItemLink(bag, slot) + if link and link:find("|Hkeystone:") then + print("[MythicReminder][debug] bag keystone: " .. tostring(link)) + end + end + end + end + end + + local function UpdateReminder(event) + if not IsEnabled() then return end + if (_G.EUI_MythicReminderDebug == true) or (EllesmereUIDB and EllesmereUIDB.mythicKeyReminderDebug) then + print("[MythicReminder][debug] UpdateReminder event=" .. tostring(event) .. " reminderActive=" .. tostring(reminderActive) .. " reminderCompleted=" .. tostring(reminderCompleted) .. " reminderMapID=" .. tostring(reminderMapID) .. " reminderLevel=" .. tostring(reminderLevel)) + end + + if event == "CHALLENGE_MODE_COMPLETED" then + if reminderActive and not reminderCompleted then + if (_G.EUI_MythicReminderDebug == true) or (EllesmereUIDB and EllesmereUIDB.mythicKeyReminderDebug) then + print("[MythicReminder][debug] CHALLENGE_MODE_COMPLETED triggered, attempting to show reminder") + end + if ShowReminder() then + reminderCompleted = true + end + end + return + end + + local activeMapID = C_ChallengeMode and C_ChallengeMode.GetActiveChallengeMapID and C_ChallengeMode.GetActiveChallengeMapID() + if activeMapID then + if not reminderActive then + StartReminder() + end + return + end + + if event == "CHALLENGE_MODE_COMPLETED" then + if reminderActive and not reminderCompleted then + reminderReadyToShow = true + if ShowReminder() then + reminderCompleted = true + end + end + return + end + + if (reminderActive or reminderCompleted) and not _isInChallengeMode() then + if reminderActive and reminderReadyToShow then + if ShowReminder() then + reminderCompleted = true + end + else + if reminderActive then + if EllesmereUI and EllesmereUI.Debug then + EllesmereUI:Debug("MythicReminder: leaving challenge mode but dungeon not complete") + else + print("[MythicReminder] leaving challenge mode but dungeon not complete") + end + end + ResetReminder() + end + end + end + + local reminderFrame = CreateFrame("Frame") + reminderFrame:RegisterEvent("PLAYER_ENTERING_WORLD") + reminderFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA") + reminderFrame:RegisterEvent("CHALLENGE_MODE_START") + reminderFrame:RegisterEvent("CHALLENGE_MODE_COMPLETED") + reminderFrame:RegisterEvent("SCENARIO_CRITERIA_UPDATE") + reminderFrame:SetScript("OnEvent", function(self, event) + UpdateReminder(event) + end) + + -- Test hook / slash command to exercise the reminder without running a full key. + _G.EUI_TestMythicReminder = function() + StartReminder() + ShowReminder() + end + SLASH_EUI_MYTHICREM1 = "/euimr" + SlashCmdList["EUI_MYTHICREM"] = function(msg) + msg = (msg or ""):lower() + if msg:match("^%s*debug%s*$") then + _G.EUI_MythicReminderDebug = not _G.EUI_MythicReminderDebug + print("[EUI] MythicReminder debug=" .. tostring(_G.EUI_MythicReminderDebug)) + return + end + if not IsEnabled() then print("Mythic Reminder disabled in QoL options") return end + StartReminder() + if msg:match("force") then + -- Force display regardless of bag contents + ShowReminder(true) + print("[EUI] Mythic Reminder forced (bypass check).") + else + ShowReminder() + print("[EUI] Mythic Reminder test executed.") + end + end + end + --------------------------------------------------------------------------- -- Train All Button --------------------------------------------------------------------------- diff --git a/EllesmereUIQoL/EllesmereUIQoL.toc b/EllesmereUIQoL/EllesmereUIQoL.toc index 2ca3311c..efdccacf 100644 --- a/EllesmereUIQoL/EllesmereUIQoL.toc +++ b/EllesmereUIQoL/EllesmereUIQoL.toc @@ -14,6 +14,7 @@ EllesmereUIQoL.lua EllesmereUIQoL_Cursor.lua EllesmereUIQoL_BattleRes.lua EllesmereUIQoL_AutoLogging.lua +EllesmereUIQoL_LFGMythicLocation.lua # Options EUI_QoL_Options.lua diff --git a/EllesmereUIQoL/EllesmereUIQoL_LFGMythicLocation.lua b/EllesmereUIQoL/EllesmereUIQoL_LFGMythicLocation.lua new file mode 100644 index 00000000..325796e0 --- /dev/null +++ b/EllesmereUIQoL/EllesmereUIQoL_LFGMythicLocation.lua @@ -0,0 +1,104 @@ +-- Main table for the addon +-- Changelog: +-- 2026-05-16: Added QoL options toggle for the Mythic LFG Location module. +-- The toggle now enables/disables event handling and prevents /lfgtest output when disabled. +-- Note: renamed file to match upstream EllesmereUIQoL module naming convention. +local LFGML = { + Name = "LFGMythicLocation", + Prefix = "|cff00ff00[LFG-Memo]|r" +} + +-- Create the event frame +local Frame = CreateFrame("Frame") +Frame:RegisterEvent("ADDON_LOADED") +Frame:RegisterEvent("LFG_LIST_APPLICATION_STATUS_UPDATED") + +-- Function to display the data with the correct layout +local function ShowDungeonInfo(dungeon, leader, title, details) + -- Separate prints ensure clean newlines and use the short [LFG-Memo] prefix + print(" ") + print(LFGML.Prefix .. " ---------------------------------------------") + print(LFGML.Prefix .. " ** INVITE ACCEPTED **") + print(LFGML.Prefix .. " Destination: |cffffffff" .. dungeon .. "|r") + print(LFGML.Prefix .. " Group Leader: |cff00ccff" .. leader .. "|r") + + -- Print Title if available (this contains things like "+7 main 3.2k rio") + if title and title ~= "" then + print(LFGML.Prefix .. " Title: |cffffd100" .. title .. "|r") + end + + -- Print Details/Comment if available + if details and details ~= "" then + print(LFGML.Prefix .. " Details: |cffb3b3b3" .. details .. "|r") + end + print(LFGML.Prefix .. " ---------------------------------------------") + print(" ") + + PlaySound(8960, "Master") +end + +-- Returns true when the feature is enabled in the QoL options. +local function IsEnabled() + return not (EllesmereUIDB and EllesmereUIDB.lfgMythicLocation == false) +end + +-- Keep the event frame enabled only when the option is active. +local function UpdateEventRegistration() + if IsEnabled() then + Frame:RegisterEvent("LFG_LIST_APPLICATION_STATUS_UPDATED") + else + Frame:UnregisterEvent("LFG_LIST_APPLICATION_STATUS_UPDATED") + end +end + +-- Exposed for the QoL options toggle to refresh this module's event registration. +_G.EUI_LFGMythicLocation_UpdateState = UpdateEventRegistration + +-- Event handler logic +Frame:SetScript("OnEvent", function(self, event, ...) + if event == "ADDON_LOADED" then + local name = ... + if name == LFGML.Name then + print(LFGML.Prefix .. " System ready. Use /lfgtest for a preview.") + UpdateEventRegistration() + self:UnregisterEvent("ADDON_LOADED") + end + + elseif event == "LFG_LIST_APPLICATION_STATUS_UPDATED" then + local searchResultID, newStatus = ... + + if newStatus == "inviteaccepted" then + local data = C_LFGList.GetSearchResultInfo(searchResultID) + + if data then + local activityInfo = C_LFGList.GetActivityInfoTable(data.activityIDs[1]) + + if activityInfo and activityInfo.categoryID == 2 then + local dungeonName = activityInfo.fullName or "Unknown Dungeon" + local leaderName = data.leaderName or "Unknown Leader" + + -- Extract the protected strings (Title and Comment/Details) + local groupTitle = data.name or "" + local groupDetails = data.comment or "" + + ShowDungeonInfo(dungeonName, leaderName, groupTitle, groupDetails) + end + end + end + end +end) + +-- Slash Command for manual testing +SLASH_LFGTEST1 = "/lfgtest" +SlashCmdList["LFGTEST"] = function() + if not IsEnabled() then + print(LFGML.Prefix .. " Mythic LFG Location is disabled.") + return + end + + -- Dynamically gets the name of the testing character + local currentCharacterName = UnitName("player") or "Katzenhirn" + + -- Simulates the exact layout output with your current character as leader + ShowDungeonInfo("Mythic Dungeon", currentCharacterName, "+7 main 3.2k rio", "Checking raider.io, bring big DPS!") +end \ No newline at end of file diff --git a/EllesmereUIUnitFrames/EUI_UnitFrames_Options.lua b/EllesmereUIUnitFrames/EUI_UnitFrames_Options.lua index 62eb9dd7..a2aaf54f 100644 --- a/EllesmereUIUnitFrames/EUI_UnitFrames_Options.lua +++ b/EllesmereUIUnitFrames/EUI_UnitFrames_Options.lua @@ -8249,10 +8249,15 @@ initFrame:SetScript("OnEvent", function(self) db.profile.boss.showCastIcon = v ReloadAndUpdate() end }, - { type="slider", text="Vertical Spacing", min=20, max=200, step=1, + { type="slider", text="Vertical Spacing", min=-200, max=200, step=1, getValue=function() return db.profile.bossSpacing or 80 end, setValue=function(v) db.profile.bossSpacing = v; ReloadAndUpdate() end }) - return castRow, eh + ch + local growthRow, gh = Ww:DualRow(pp, yy - eh - ch, + { type="dropdown", text="Stack Direction", values={ down="Down", up="Up" }, order={ "down", "up" }, + getValue=function() return db.profile.boss.bossStackDirection or "down" end, + setValue=function(v) db.profile.boss.bossStackDirection = v; ReloadAndUpdate() end }, + { type="spacer" }) + return growthRow, eh + ch + gh end local function bossAfterSize(Ww, pp, yy) diff --git a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua index 3fa574f9..53aa6371 100644 --- a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua +++ b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua @@ -705,6 +705,7 @@ local defaults = { raidMarkerAlign = "left", raidMarkerX = 0, raidMarkerY = 0, + bossStackDirection = "down", healthReverseFill = false, }, enabledFrames = { @@ -5193,7 +5194,12 @@ local function ReloadFrames() local prev = frames["boss" .. (bossIdx - 1)] if prev then frame:ClearAllPoints() - frame:SetPoint("TOPLEFT", prev, "TOPLEFT", 0, -bossSpacing) + local bossStackDir = db.profile.boss and db.profile.boss.bossStackDirection or "down" + if bossStackDir == "up" then + frame:SetPoint("BOTTOMLEFT", prev, "TOPLEFT", 0, bossSpacing) + else + frame:SetPoint("TOPLEFT", prev, "TOPLEFT", 0, -bossSpacing) + end end end else @@ -7679,6 +7685,7 @@ function InitializeFrames() local barHeight = (bossSettings.healthHeight or 34) + (bossSettings.powerHeight or 6) + (bossSettings.castbarHeight or 14) local gap = 10 local spacing = db.profile.bossSpacing or (barHeight + gap) + local bossStackDir = db.profile.boss and db.profile.boss.bossStackDirection or "down" for i = 1, 5 do local bossUnit = "boss" .. i local bossFrame = oUF:Spawn(bossUnit, "EllesmereUIUnitFrames_Boss" .. i) @@ -7696,7 +7703,11 @@ function InitializeFrames() local prev = frames["boss" .. (i - 1)] if prev then bossFrame:ClearAllPoints() - bossFrame:SetPoint("TOPLEFT", prev, "TOPLEFT", 0, -spacing) + if bossStackDir == "up" then + bossFrame:SetPoint("BOTTOMLEFT", prev, "TOPLEFT", 0, spacing) + else + bossFrame:SetPoint("TOPLEFT", prev, "TOPLEFT", 0, -spacing) + end end end @@ -8386,6 +8397,8 @@ function SetupOptionsPanel() if EllesmereUI._unlockActive then return end if k == "boss" then local spacing = db.profile.bossSpacing or 60 + local bossStackDir = db.profile.boss and db.profile.boss.bossStackDirection or "down" + local bossAnchorPoint = (bossStackDir == "up") and "BOTTOMLEFT" or "TOPLEFT" -- boss1 to UIParent; chain 2..5 from the previous boss. if frames.boss1 then frames.boss1:ClearAllPoints() @@ -8396,7 +8409,7 @@ function SetupOptionsPanel() local prev = frames["boss" .. (i - 1)] if bf and prev then bf:ClearAllPoints() - bf:SetPoint("TOPLEFT", prev, "TOPLEFT", 0, -spacing) + bf:SetPoint(bossAnchorPoint, prev, "TOPLEFT", 0, (bossStackDir == "up") and spacing or -spacing) end end elseif k == "classPower" then @@ -8441,6 +8454,8 @@ function SetupOptionsPanel() end if k == "boss" then local spacing = db.profile.bossSpacing or 60 + local bossStackDir = db.profile.boss and db.profile.boss.bossStackDirection or "down" + local bossAnchorPoint = (bossStackDir == "up") and "BOTTOMLEFT" or "TOPLEFT" if frames.boss1 then local bx, by = SnapForFrame(frames.boss1, pos.x, pos.y) frames.boss1:ClearAllPoints() @@ -8451,7 +8466,7 @@ function SetupOptionsPanel() local prev = frames["boss" .. (i - 1)] if bf and prev then bf:ClearAllPoints() - bf:SetPoint("TOPLEFT", prev, "TOPLEFT", 0, -spacing) + bf:SetPoint(bossAnchorPoint, prev, "TOPLEFT", 0, (bossStackDir == "up") and spacing or -spacing) end end elseif k == "classPower" then diff --git a/EllesmereUI_Widgets.lua b/EllesmereUI_Widgets.lua index 0701bee9..f91b7b4e 100644 --- a/EllesmereUI_Widgets.lua +++ b/EllesmereUI_Widgets.lua @@ -2696,6 +2696,11 @@ function WidgetFactory:DualRow(parent, yOffset, leftCfg, rightCfg) local function BuildHalf(region, cfg) if not cfg then return end local t = cfg.type + -- Empty half-space placeholder for dual/third rows. + if t == "spacer" then + region._control = nil + return + end -- Label (all types have one) local label = MakeFont(region, 14, nil, TEXT_WHITE_R, TEXT_WHITE_G, TEXT_WHITE_B) PP.Point(label, "LEFT", region, "LEFT", SIDE_PAD, 0)