diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c7bcbae..11bfab4 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -10,7 +10,7 @@ AstralRaidLeader is a **World of Warcraft (Retail) addon** written in Lua. It ma |---|---| | `AstralRaidLeader.lua` | Core logic: event handling, auto-promote, guild rank resolution, consumable audit, death tracking, slash commands | | `AstralRaidLeader_Options.lua` | In-game settings window (760×500 custom frame) | -| `AstralRaidLeader_Deaths.lua` | Death recap window (520×430 custom frame) | +| `AstralRaidLeader_Deaths.lua` | Death recap window (640×430 custom frame) | | `AstralRaidLeader.toc` | Addon manifest; load order is `.lua` → `_Options.lua` → `_Deaths.lua` | The addon namespace is exposed as `_G["AstralRaidLeader"]` and referenced as `ARL` in every file. @@ -122,7 +122,7 @@ frame (760×500, DIALOG strata, level 100) - `panels[3]` – Guild Ranks - `panels[4]` – Consumables - `panels[5]` – Deaths settings -- `panels[6]` – Raid Groups (import, select, preview, apply) +- `panels[6]` – Raid Groups (import, dropdown select, auto-apply toggle, preview, apply) - `panels[7]` – Raid Groups Settings (output/apply behavior toggles) **Main tab → sub-tabs mapping** is defined in `MAIN_TABS` and drives `SelectMainTab` / `SelectSubTab`. @@ -130,7 +130,7 @@ frame (760×500, DIALOG strata, level 100) ### Death Recap Window (`AstralRaidLeader_Deaths.lua`) ``` -frame (520×430, DIALOG strata, level 110) +frame (640×430, DIALOG strata, level 110) ├── header (same pattern as Options) ├── topCloseButton ├── dragRegion @@ -254,3 +254,5 @@ Sets muted text color on `cb.Text`, brightens on hover. Idempotent via `cb._arlS 12. **Cross-instance consumable false positives** — do not audit buffs for units outside your phase/instance; they will appear missing by default. 13. **Raid-group actions in combat** — import/select/apply/delete/clear interactions for raid layouts must be blocked while in combat. 14. **Auto-apply invite spam** — when auto-applying on member join, do not re-send invites for every roster update; subgroup apply can run without invite side effects. +15. **Raid layout selector implementation** — panel 6 uses Blizzard `UIDropDownMenuTemplate`, not a custom button list. Preserve click-anywhere-to-open behavior and left-aligned selected-text styling. +16. **Difficulty mismatch behavior** — applying a raid layout must fail with a clear message when current raid difficulty does not match the layout's imported difficulty. diff --git a/AstralRaidLeader.lua b/AstralRaidLeader.lua index 60300b1..b9625f6 100644 --- a/AstralRaidLeader.lua +++ b/AstralRaidLeader.lua @@ -589,6 +589,89 @@ local function BuildRaidLayoutTargets(profile, snapshot) } end +local RAID_DIFFICULTY_IDS = { + lfr = { [17] = true }, + normal = { [14] = true }, + heroic = { [15] = true }, + mythic = { [16] = true }, +} + +local function NormalizeDifficultyToken(value) + local token = Trim(value):lower() + token = token:gsub("%s+", "") + + if token == "" or token == "unknown" then + return "" + elseif token == "mythic" or token == "m" or token == "16" then + return "mythic" + elseif token == "heroic" or token == "h" or token == "15" then + return "heroic" + elseif token == "normal" or token == "n" or token == "14" then + return "normal" + elseif token == "lfr" or token == "17" then + return "lfr" + end + + return token +end + +local function GetCurrentRaidDifficultyInfo() + local raidDifficultyID = 0 + if _G.GetRaidDifficultyID then + raidDifficultyID = tonumber(_G.GetRaidDifficultyID()) or 0 + end + + local difficultyName = "" + if raidDifficultyID > 0 and _G.GetDifficultyInfo then + local name = _G.GetDifficultyInfo(raidDifficultyID) + difficultyName = Trim(name) + end + + if (raidDifficultyID <= 0 or difficultyName == "") and _G.GetInstanceInfo then + local _, _, difficultyID, difficultyText = _G.GetInstanceInfo() + if raidDifficultyID <= 0 then + raidDifficultyID = tonumber(difficultyID) or 0 + end + if difficultyName == "" then + difficultyName = Trim(difficultyText) + end + end + + return raidDifficultyID, difficultyName +end + +local function IsRaidLayoutDifficultyMatch(profile) + local expectedToken = NormalizeDifficultyToken(profile and profile.difficulty) + if expectedToken == "" then + return true + end + + local currentID, currentName = GetCurrentRaidDifficultyInfo() + local expectedIDs = RAID_DIFFICULTY_IDS[expectedToken] + if expectedIDs and currentID > 0 then + if expectedIDs[currentID] then + return true + end + end + + local currentToken = NormalizeDifficultyToken(currentName) + if currentToken ~= "" and currentToken == expectedToken then + return true + end + + local shownCurrent = Trim(currentName) + if shownCurrent == "" then + shownCurrent = currentID > 0 and ("ID " .. tostring(currentID)) or "Unknown" + end + + return false, string.format( + "Raid layout |cffffd100%s|r is for |cffffd100%s|r, but current raid difficulty is |cffffd100%s|r.", + GetRaidLayoutLabel(profile), + Trim(profile.difficulty), + shownCurrent + ) +end + local function StopRaidLayoutApply(message) ARL.raidLayoutApplyState = nil if message and message ~= "" then @@ -777,6 +860,11 @@ local function ApplyRaidLayoutProfile(profile, options) "You must be the raid leader or an assistant to apply a raid layout." end + local difficultyOK, difficultyErr = IsRaidLayoutDifficultyMatch(profile) + if not difficultyOK then + return false, difficultyErr + end + local snapshot = GetRaidRosterSnapshot() if #snapshot.entries == 0 then return false, "No raid roster data is available yet. Try again in a moment." diff --git a/AstralRaidLeader_Deaths.lua b/AstralRaidLeader_Deaths.lua index 90be646..a007b0e 100644 --- a/AstralRaidLeader_Deaths.lua +++ b/AstralRaidLeader_Deaths.lua @@ -321,12 +321,22 @@ local function BuildDeathLine(i, entry) COLOR_PLAYER, entry.playerName, COLOR_RESET ) + local spellId = entry.spellId + local mechanicName = entry.mechanic + if (not mechanicName or mechanicName == "" or mechanicName == "...") + and spellId and spellId > 0 + then + local resolvedName = ResolveSpellNameAndIcon(spellId) + if resolvedName and resolvedName ~= "" then + mechanicName = resolvedName + end + end + local spellText = string.format( "%s%s%s", - COLOR_MECHANIC, entry.mechanic, COLOR_RESET + COLOR_MECHANIC, mechanicName or "Unknown", COLOR_RESET ) - local spellId = entry.spellId if spellId and spellId > 0 then local _, icon = ResolveSpellNameAndIcon(spellId) if icon then @@ -355,14 +365,21 @@ local function PopulateDeathRow(row, i, entry) row.spellButton:EnableMouse(hasSpellTooltip) local desiredSpellW = SafeWidth(row.spellButton.text:GetStringWidth()) + 2 - local spellW = math.max(24, math.min(desiredSpellW, 300)) -- cap at 300 to prevent overflow + local prefixW = SafeWidth(row.prefixText:GetStringWidth()) + local suffixW = SafeWidth(row.suffixText:GetStringWidth()) + local rowW = SafeWidth(row:GetWidth()) + if rowW <= 0 then + rowW = 470 + end + local availableSpellW = math.max(24, rowW - prefixW - suffixW - 14) + local spellW = math.max(24, math.min(desiredSpellW, availableSpellW)) row.spellButton:ClearAllPoints() row.spellButton:SetPoint("LEFT", row.prefixText, "RIGHT", 4, 0) row.spellButton:SetWidth(spellW) row.suffixText:ClearAllPoints() - row.suffixText:SetPoint("LEFT", row.spellButton, "RIGHT", 2, 0) + row.suffixText:SetPoint("RIGHT", row, "RIGHT", 0, 0) end local function RefreshRecap() diff --git a/AstralRaidLeader_Options.lua b/AstralRaidLeader_Options.lua index f1515fa..9111254 100644 --- a/AstralRaidLeader_Options.lua +++ b/AstralRaidLeader_Options.lua @@ -5,6 +5,12 @@ local ARL = _G["AstralRaidLeader"] if not ARL then return end local ChatFontNormal = _G.ChatFontNormal +local UIDropDownMenu_SetWidth = _G.UIDropDownMenu_SetWidth +local UIDropDownMenu_SetText = _G.UIDropDownMenu_SetText +local UIDropDownMenu_Initialize = _G.UIDropDownMenu_Initialize +local UIDropDownMenu_CreateInfo = _G.UIDropDownMenu_CreateInfo +local UIDropDownMenu_AddButton = _G.UIDropDownMenu_AddButton +local ToggleDropDownMenu = _G.ToggleDropDownMenu local function Print(msg) print("|cff00ccff[AstralRaidLeader]|r " .. tostring(msg)) @@ -814,7 +820,7 @@ raidLayoutInfoText:SetText( local raidImportInset = CreateFrame("Frame", nil, p6, BackdropTemplateMixin and "BackdropTemplate" or nil) raidImportInset:SetPoint("TOPLEFT", 8, -32) -raidImportInset:SetSize(528, 128) +raidImportInset:SetSize(528, 88) SkinPanel(raidImportInset, 0.07, 0.10, 0.14, 0.34, 0.22, 0.28, 0.36, 0.24) local raidImportScroll = CreateFrame( @@ -850,7 +856,7 @@ end) raidImportScroll:SetScrollChild(raidImportEdit) local importRaidLayoutsButton = CreateFrame("Button", nil, p6, "UIPanelButtonTemplate") -importRaidLayoutsButton:SetPoint("TOPLEFT", 8, -170) +importRaidLayoutsButton:SetPoint("TOPLEFT", 8, -130) importRaidLayoutsButton:SetSize(100, 24) importRaidLayoutsButton:SetText("Import Note") @@ -874,31 +880,65 @@ clearRaidLayoutsButton:SetPoint("LEFT", deleteRaidLayoutButton, "RIGHT", 8, 0) clearRaidLayoutsButton:SetSize(100, 24) clearRaidLayoutsButton:SetText("Clear Saved") +local raidGroupAutoApplyOnJoinListCB = CreateCheckbox(p6, + "Enable auto-apply for selected layout when a member joins", + "When enabled, the selected layout is re-applied whenever a new raid member joins.", + 8, -162) + local raidLayoutListLabel = p6:CreateFontString(nil, "ARTWORK", "GameFontNormal") -raidLayoutListLabel:SetPoint("TOPLEFT", 8, -206) -raidLayoutListLabel:SetText("Saved raid layouts (click to select)") +raidLayoutListLabel:SetPoint("TOPLEFT", 8, -190) +raidLayoutListLabel:SetText("Saved raid layout") -local MAX_RAID_LAYOUT_BUTTONS = 10 -local raidLayoutButtons = {} -for _i = 1, MAX_RAID_LAYOUT_BUTTONS do - local col = (_i - 1) % 2 - local row = math.floor((_i - 1) / 2) - local btn = CreateFrame("Button", nil, p6, "UIPanelButtonTemplate") - btn:SetPoint("TOPLEFT", 8 + col * 266, -224 + row * -22) - btn:SetSize(258, 20) - btn:SetText("") - btn:Hide() - raidLayoutButtons[_i] = btn +local raidLayoutDropDown = CreateFrame( + "Frame", + "AstralRaidLeaderRaidLayoutDropDown", + p6, + "UIDropDownMenuTemplate" +) +raidLayoutDropDown:SetPoint("TOPLEFT", p6, "TOPLEFT", -8, -208) +UIDropDownMenu_SetWidth(raidLayoutDropDown, 492) +UIDropDownMenu_SetText(raidLayoutDropDown, "No saved raid layouts") +raidLayoutDropDown:EnableMouse(false) + +local raidLayoutDropDownButton = _G["AstralRaidLeaderRaidLayoutDropDownButton"] +if raidLayoutDropDownButton then + raidLayoutDropDownButton:EnableMouse(true) + raidLayoutDropDownButton:SetHitRectInsets(0, 0, 0, 0) + raidLayoutDropDownButton:SetScript("OnClick", function() + if InCombatLockdown() then + Print("Cannot change the selected raid layout while in combat.") + return + end + if ToggleDropDownMenu then + ToggleDropDownMenu(1, nil, raidLayoutDropDown) + end + end) +end + +raidLayoutDropDown:SetScript("OnMouseDown", function(_, mouseButton) + if mouseButton == "LeftButton" and ToggleDropDownMenu and not InCombatLockdown() then + ToggleDropDownMenu(1, nil, raidLayoutDropDown) + elseif mouseButton == "LeftButton" and InCombatLockdown() then + Print("Cannot change the selected raid layout while in combat.") + end +end) + +local raidLayoutDropDownText = _G["AstralRaidLeaderRaidLayoutDropDownText"] +if raidLayoutDropDownText then + raidLayoutDropDownText:ClearAllPoints() + raidLayoutDropDownText:SetPoint("LEFT", raidLayoutDropDown, "LEFT", 32, 2) + raidLayoutDropDownText:SetPoint("RIGHT", raidLayoutDropDown, "RIGHT", -43, 2) + raidLayoutDropDownText:SetJustifyH("LEFT") end local noRaidLayoutsText = p6:CreateFontString(nil, "ARTWORK", "GameFontDisableSmall") -noRaidLayoutsText:SetPoint("TOPLEFT", 8, -228) +noRaidLayoutsText:SetPoint("TOPLEFT", 8, -244) noRaidLayoutsText:SetText("") noRaidLayoutsText:Hide() local raidLayoutPreviewInset = CreateFrame("Frame", nil, p6, BackdropTemplateMixin and "BackdropTemplate" or nil) -raidLayoutPreviewInset:SetPoint("TOPLEFT", 8, -334) -raidLayoutPreviewInset:SetSize(528, 64) +raidLayoutPreviewInset:SetPoint("TOPLEFT", 8, -254) +raidLayoutPreviewInset:SetPoint("BOTTOMRIGHT", p6, "BOTTOMRIGHT", -8, 8) SkinPanel(raidLayoutPreviewInset, 0.07, 0.10, 0.14, 0.34, 0.22, 0.28, 0.36, 0.24) local raidLayoutPreviewScroll = CreateFrame( @@ -947,16 +987,11 @@ local raidGroupApplyBehaviorLabel = p7:CreateFontString(nil, "ARTWORK", "GameFon raidGroupApplyBehaviorLabel:SetPoint("TOPLEFT", 8, -68) raidGroupApplyBehaviorLabel:SetText("Apply behaviour") -local raidGroupAutoApplyOnJoinCB = CreateCheckbox(p7, - "Auto-apply selected layout when a member joins", - "Automatically re-applies the currently selected raid layout whenever a new member joins the raid.", - 8, -92) - local raidGroupInviteMissingPlayersCB = CreateCheckbox(p7, "Invite listed players not already in the raid on apply", "When enabled, applying the selected raid layout also invites listed players" .. " who are not already in the group.", - 8, -120) + 8, -92) for _, cb in ipairs({ autoCB, reminderCB, notifyCB, notifySoundCB, quietCB, @@ -964,8 +999,8 @@ for _, cb in ipairs({ useGuildRankCB, consumableAuditCB, deathTrackingCB, showRecapCB, showRecapOnAnyEndCB, deathGroupRaidCB, deathGroupPartyCB, deathGroupGuildRaidCB, deathGroupGuildPartyCB, + raidGroupAutoApplyOnJoinListCB, raidGroupShowMissingNamesCB, - raidGroupAutoApplyOnJoinCB, raidGroupInviteMissingPlayersCB, }) do StyleCheckbox(cb) @@ -991,10 +1026,6 @@ for _, btn in ipairs(guildRankButtons) do SkinActionButton(btn) end -for _, btn in ipairs(raidLayoutButtons) do - SkinActionButton(btn) -end - -- ============================================================ -- Refresh helpers -- ============================================================ @@ -1102,19 +1133,13 @@ local function RefreshConsumableListText() end local function RefreshRaidLayoutUI() - local lastVisibleButton = nil - - for _, btn in ipairs(raidLayoutButtons) do - btn:Hide() - btn._raidLayoutKey = nil - end - noRaidLayoutsText:Hide() applyRaidLayoutButton:Disable() deleteRaidLayoutButton:Disable() clearRaidLayoutsButton:Disable() if not ARL.db then + UIDropDownMenu_SetText(raidLayoutDropDown, "Waiting for saved variables to load...") raidLayoutPreviewText:SetText("Waiting for saved variables to load...") return end @@ -1122,47 +1147,29 @@ local function RefreshRaidLayoutUI() if #ARL.db.raidLayouts == 0 then noRaidLayoutsText:SetText("No saved raid layouts. Paste a Viserio note above and click Import Note.") noRaidLayoutsText:Show() - raidLayoutPreviewInset:ClearAllPoints() - raidLayoutPreviewInset:SetPoint("TOPLEFT", noRaidLayoutsText, "BOTTOMLEFT", 0, -12) + UIDropDownMenu_SetText(raidLayoutDropDown, "No saved raid layouts") raidLayoutPreviewText:SetText("Selected layout preview will appear here.") - raidLayoutPreviewContent:SetHeight(56) + raidLayoutPreviewContent:SetHeight(104) raidLayoutPreviewScroll:SetVerticalScroll(0) return end clearRaidLayoutsButton:Enable() - for i, profile in ipairs(ARL.db.raidLayouts) do - local btn = raidLayoutButtons[i] - if not btn then break end - btn._raidLayoutKey = profile.key - local marker = (profile.key == ARL.db.activeRaidLayoutKey) and "> " or "" - btn:SetText(string.format( - "%s%s", - marker, - ARL.GetRaidLayoutLabel and ARL.GetRaidLayoutLabel(profile) or (profile.name or "Unknown") - )) - btn:Show() - lastVisibleButton = btn - end - - raidLayoutPreviewInset:ClearAllPoints() - raidLayoutPreviewInset:SetPoint( - "TOPLEFT", - lastVisibleButton or raidLayoutListLabel, - "BOTTOMLEFT", - 0, - lastVisibleButton and -12 or -32 - ) - local active = ARL.GetActiveRaidLayoutProfile and ARL.GetActiveRaidLayoutProfile() or nil if not active then - raidLayoutPreviewText:SetText("Click a saved layout to select it.") - raidLayoutPreviewContent:SetHeight(56) + UIDropDownMenu_SetText(raidLayoutDropDown, "None (disabled)") + raidLayoutPreviewText:SetText("Select a saved layout from the dropdown.") + raidLayoutPreviewContent:SetHeight(104) raidLayoutPreviewScroll:SetVerticalScroll(0) return end + UIDropDownMenu_SetText( + raidLayoutDropDown, + ARL.GetRaidLayoutLabel and ARL.GetRaidLayoutLabel(active) or (active.name or "Unknown") + ) + applyRaidLayoutButton:Enable() deleteRaidLayoutButton:Enable() @@ -1180,11 +1187,63 @@ local function RefreshRaidLayoutUI() local width = 486 local height = raidLayoutPreviewText:GetStringHeight() + 8 - raidLayoutPreviewContent:SetSize(width, math.max(56, height)) + raidLayoutPreviewContent:SetSize(width, math.max(104, height)) raidLayoutPreviewScroll:UpdateScrollChildRect() raidLayoutPreviewScroll:SetVerticalScroll(0) end +UIDropDownMenu_Initialize(raidLayoutDropDown, function(_, level) + if level ~= 1 or not ARL.db then + return + end + + local activeKey = ARL.db.activeRaidLayoutKey or "" + + local noneInfo = UIDropDownMenu_CreateInfo() + noneInfo.text = "None (disabled)" + noneInfo.checked = activeKey == "" + noneInfo.func = function() + if InCombatLockdown() then + Print("Cannot change the selected raid layout while in combat.") + return + end + ARL.db.activeRaidLayoutKey = "" + RefreshRaidLayoutUI() + Print("Cleared selected raid layout.") + end + UIDropDownMenu_AddButton(noneInfo, level) + + for _, profile in ipairs(ARL.db.raidLayouts or {}) do + local info = UIDropDownMenu_CreateInfo() + info.text = ARL.GetRaidLayoutLabel and ARL.GetRaidLayoutLabel(profile) or (profile.name or "Unknown") + info.checked = profile.key == activeKey + info.func = function() + if InCombatLockdown() then + Print("Cannot change the selected raid layout while in combat.") + return + end + if ARL.db.activeRaidLayoutKey == profile.key then + ARL.db.activeRaidLayoutKey = "" + RefreshRaidLayoutUI() + Print("Cleared selected raid layout.") + return + end + if not ARL.SetActiveRaidLayoutByQuery then return end + local ok, result = ARL.SetActiveRaidLayoutByQuery(profile.key) + if not ok then + Print(result) + return + end + RefreshRaidLayoutUI() + Print(string.format( + "Selected raid layout |cffffd100%s|r.", + ARL.GetRaidLayoutLabel and ARL.GetRaidLayoutLabel(result) or (result.name or "Unknown") + )) + end + UIDropDownMenu_AddButton(info, level) + end +end) + local function RefreshUI() if not ARL.db then return end @@ -1216,8 +1275,8 @@ local function RefreshUI() deathGroupGuildRaidCB:SetChecked(dft.guild_raid and true or false) deathGroupGuildPartyCB:SetChecked(dft.guild_party and true or false) + raidGroupAutoApplyOnJoinListCB:SetChecked(ARL.db.raidGroupAutoApplyOnJoin == true) raidGroupShowMissingNamesCB:SetChecked(ARL.db.raidGroupShowMissingNames ~= false) - raidGroupAutoApplyOnJoinCB:SetChecked(ARL.db.raidGroupAutoApplyOnJoin == true) raidGroupInviteMissingPlayersCB:SetChecked(ARL.db.raidGroupInviteMissingPlayers == true) RefreshListText() @@ -1782,26 +1841,6 @@ clearRaidLayoutsButton:SetScript("OnClick", function() Print("Cleared all saved raid layouts.") end) -for _, btn in ipairs(raidLayoutButtons) do - btn:SetScript("OnClick", function(self) - if InCombatLockdown() then - Print("Cannot change the selected raid layout while in combat.") - return - end - if not self._raidLayoutKey or not ARL.SetActiveRaidLayoutByQuery then return end - local ok, result = ARL.SetActiveRaidLayoutByQuery(self._raidLayoutKey) - if not ok then - Print(result) - return - end - RefreshRaidLayoutUI() - Print(string.format( - "Selected raid layout |cffffd100%s|r.", - ARL.GetRaidLayoutLabel and ARL.GetRaidLayoutLabel(result) or (result.name or "Unknown") - )) - end) -end - -- ============================================================ -- ShowOptions -- ============================================================ @@ -1814,7 +1853,7 @@ raidGroupShowMissingNamesCB:SetScript("OnClick", function(self) ARL.db.raidGroupShowMissingNames and "enabled" or "disabled")) end) -raidGroupAutoApplyOnJoinCB:SetScript("OnClick", function(self) +raidGroupAutoApplyOnJoinListCB:SetScript("OnClick", function(self) if updating or not ARL.db then return end ARL.db.raidGroupAutoApplyOnJoin = self:GetChecked() and true or false Print(string.format("Auto-apply on join |cff%s%s|r.", diff --git a/README.md b/README.md index c6661ee..31a0959 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ In-game settings window for configuring auto-promote, reminder behavior, popup n - **List reordering** – move preferred leaders up or down in priority using slash commands or the **Move Up** / **Move Down** buttons in the settings window; no need to remove and re-add entries. - **Group-type filters (multi-select)** – independently toggle auto-promote and death-recap capture for raids, parties, guild raids, and guild parties. You can enable any combination. - **Consumable audit** – when a ready check is initiated, the addon scans every queryable group member's active buffs and prints a report of who is missing tracked consumable categories (e.g. Flask, Food). Members outside your current instance/phase are skipped to avoid false positives. Consumable categories are fully configurable via `/arl consumable add`. The audit can be toggled on or off without affecting any other feature. -- **Raid group layouts** – import Viserio-style encounter notes, save each encounter's 20-player ordering, and apply the subgroup layout to your current raid. Listed players are assigned into groups 1-4 by note order, while any current raiders not in the note are packed into groups 8, 7, 6, and 5. Raid-group actions are blocked in combat, and optional settings let you auto-apply on joins and invite missing listed players on apply. +- **Raid group layouts** – import Viserio-style encounter notes, save each encounter's 20-player ordering, and apply the subgroup layout to your current raid. Use the Raid Groups dropdown to select a layout (or choose **None (disabled)**). Listed players are assigned into groups 1-4 by note order, while any current raiders not in the note are packed into groups 8, 7, 6, and 5. Raid-group actions are blocked in combat, and optional settings let you auto-apply on joins and invite missing listed players on apply. +- **Raid layout difficulty guard** – imported layouts are keyed by encounter + difficulty and only apply when the current raid difficulty matches the layout's saved difficulty. - **Death recap** – records wipe deaths and displays them in a recap window (`/arl deaths`). In current Midnight-compatible builds, death data is sourced from the built-in `C_DamageMeter` combat session API. - **Quiet mode** – suppress all addon chat output so auto-promotion happens silently in the background. - **Persistent settings** – your list and preferences are saved between sessions via `SavedVariables`. @@ -103,19 +104,23 @@ If no character from the preferred leaders list is in the group, the addon will Use `/arl deaths` to open the last wipe recap window. -The recap records who died and when during a failed encounter attempt. Death source/mechanic data is pulled from the built-in `C_DamageMeter` combat session API when available. +The recap records who died and when during a failed encounter attempt. Death source/mechanic data is pulled from the built-in `C_DamageMeter` combat session API when available. When recap payloads include a spell ID but a placeholder mechanic label (for example `...`), the UI resolves and displays the spell name. ### Raid group layouts Open the settings window and use the `Raid Groups` tab to paste a Viserio note that contains one or more encounter blocks such as `EncounterID:3176;Difficulty:Mythic;Name:Averzian` followed by an `invitelist:` line. -When imported, each encounter is saved separately. Applying a saved layout assigns the listed players into raid groups in note order, five players per group, and places any current raiders who were not listed into groups 8, 7, 6, and 5 as those groups fill. +When imported, each encounter is saved separately. Select one via the dropdown (or set it to **None (disabled)**). Applying a saved layout assigns the listed players into raid groups in note order, five players per group, and places any current raiders who were not listed into groups 8, 7, 6, and 5 as those groups fill. + +Saved layouts only apply when the current raid difficulty matches the layout's imported difficulty. In `Raid Groups -> Settings`, you can configure: - showing missing-player names in apply output -- auto-applying the selected layout when a new member joins - inviting listed players not already in raid when you apply +In `Raid Groups` (main panel), you can configure: +- auto-applying the selected layout when a new member joins + The invite-on-apply option is disabled by default. Auto-apply-on-join re-runs subgroup placement only and does not repeatedly send invites. ## How it works