Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -122,15 +122,15 @@ 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`.

### 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
Expand Down Expand Up @@ -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.
88 changes: 88 additions & 0 deletions AstralRaidLeader.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."
Expand Down
25 changes: 21 additions & 4 deletions AstralRaidLeader_Deaths.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
Loading
Loading