Files
ShaguTweaks-Extras-ClassicAPI/mods/raid.lua
T
2026-08-30 11:18:37 +02:00

664 lines
19 KiB
Lua

local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local API = ShaguTweaks.API
local module = ShaguTweaks:register({
title = T["Enable Raid Frames"],
description = T["Very simple raid frames with only the most basic features."],
expansions = { ["vanilla"] = true, ["tbc"] = false },
category = T["Raid Frames"],
maintainer = "@shagu (GitHub)",
enabled = true,
config = {
["raid.width"] = 64,
["raid.height"] = 32,
["raid.rows"] = 10,
["raid.scale"] = 1,
}
})
local components = {}
local raidReadyCallbacks = {}
local raidReadyFrame
-- Raid submodules are enabled from an unordered ShaguTweaks.mods table.
-- Queue work that needs the actual raid frame so submodules never depend on
-- which module happens to be visited first by pairs().
ShaguTweaks.RaidFrame_OnReady = function(callback)
if type(callback) ~= "function" then return end
if raidReadyFrame then
callback(raidReadyFrame)
else
table.insert(raidReadyCallbacks, callback)
end
end
local function NotifyRaidFrameReady(raid)
raidReadyFrame = raid
local callbacks = raidReadyCallbacks
raidReadyCallbacks = {}
for i = 1, table.getn(callbacks) do
callbacks[i](raid)
end
end
local backdrop = {
border = {
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 12,
insets = { left = 2, right = 2, top = 2, bottom = 2 }
},
background = {
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
tile = true, tileSize = 16, edgeSize = 12,
insets = { left = 2, right = 2, top = 2, bottom = 2 }
}
}
-- Helper Functions
local GetFramePosition = function(i, rows)
local left = math.ceil(i/rows)
local top = math.mod(i, rows)
top = top == 0 and rows or top
return left, top
end
local UnitInRange = function(unitstr)
if not unitstr then return false end
-- ClassicAPI provides a reach-aware 40-yard range check. Any compatibility
-- fallback is centralized in ShaguTweaks.API rather than duplicated here.
local inRange, checked = API.UnitInRange(unitstr)
return checked and inRange or false
end
-- Unit Frames
local UnitFrame_NewComponent = function(name, object)
object.name = name
table.insert(components, object)
end
local UnitFrame_Refresh = function(frame)
if not frame or not frame.unitstr or not UnitName(frame.unitstr) then return end
for _, component in ipairs(components) do
component.update(frame, "FRAME_REFRESH")
end
end
local UnitFrame_OnEnter = function()
if not this.unitstr then return end
GameTooltip_SetDefaultAnchor(GameTooltip, this)
GameTooltip:SetUnit(this.unitstr)
GameTooltip:Show()
end
local UnitFrame_OnLeave = function()
if not this.unitstr then return end
GameTooltip:Hide()
end
local UnitFrame_OnUpdate = function()
if not this.unitstr or not UnitName(this.unitstr) then
this:Hide()
return
end
-- Only true frame-by-frame animations run on individual unit frames.
for component in pairs(this.onupdate) do
component.update(this)
end
end
local UnitFrame_OnClick = function()
if not this.unitstr then return end
local button = arg1
-- handle special cases
if SpellIsTargeting() then
if button == "LeftButton" then
SpellTargetUnit(this.unitstr)
return
elseif button == "RightButton" then
SpellStopTargeting()
return
end
elseif CursorHasItem() then
DropItemOnUnit(this.unitstr)
return
end
-- hide previous menus
HideDropDownMenu(1)
-- default click actions
if button == "LeftButton" then
TargetUnit(this.unitstr)
elseif button == "RightButton" then
local unitstr = this.unitstr
local name = UnitName(this.unitstr)
FriendsDropDown.displayMode = "MENU"
FriendsDropDown.initialize = function()
UnitPopup_ShowMenu(_G[UIDROPDOWNMENU_OPEN_MENU], "PARTY", unitstr, name)
end
ToggleDropDownMenu(1, nil, FriendsDropDown, "cursor")
end
end
local UnitFrame_OnEvent = function()
-- break on empty units or empty events
if not this.unitstr then return end
if not this.events[event] then return end
-- run update functions for each frame
for _, component in ipairs(this.events[event]) do
component.update(this, event)
end
end
local CreateUnitFrame = function(parent, i)
local frame = parent.frames[i] or CreateFrame("Button", "ShaguTweaksRaidUnitFrame"..i, parent)
local left, top = GetFramePosition(i, parent.config["raid.rows"])
frame:SetPoint("TOPLEFT", (left-1)*(parent.config["raid.width"]+2) + 4, -(top-1)*(parent.config["raid.height"] + 1) - 4)
frame:SetWidth(parent.config["raid.width"])
frame:SetHeight(parent.config["raid.height"])
frame.unitstr = "raid"..i
frame.left = left
frame.top = top
frame.events = {
['RAID_ROSTER_UPDATE'] = { },
['PARTY_MEMBERS_CHANGED'] = { },
['PLAYER_ENTERING_WORLD'] = { },
}
frame.onupdate = {}
-- assign required events and scripts
frame:SetScript("OnEvent", UnitFrame_OnEvent)
frame:SetScript("OnClick", UnitFrame_OnClick)
frame:SetScript("OnEnter", UnitFrame_OnEnter)
frame:SetScript("OnLeave", UnitFrame_OnLeave)
frame:RegisterForClicks('LeftButtonUp', 'RightButtonUp')
-- base events
frame:RegisterEvent("RAID_ROSTER_UPDATE")
frame:RegisterEvent("PARTY_MEMBERS_CHANGED")
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
for _, object in ipairs(components) do
-- create component frames
object.create(frame)
-- register component update events
for _, event in ipairs(object.events) do
frame:RegisterEvent(event)
frame.events[event] = frame.events[event] or {}
table.insert(frame.events[event], object)
end
-- register components to base events
table.insert(frame.events['RAID_ROSTER_UPDATE'], object)
table.insert(frame.events['PARTY_MEMBERS_CHANGED'], object)
table.insert(frame.events['PLAYER_ENTERING_WORLD'], object)
-- only register true frame-by-frame components
if object.onupdate then
frame.onupdate[object] = true
end
end
if next(frame.onupdate) then
frame:SetScript("OnUpdate", UnitFrame_OnUpdate)
else
frame:SetScript("OnUpdate", nil)
end
-- save frame to parent
parent.frames[i] = frame
end
-- Components
UnitFrame_NewComponent('health', {
events = {
'UNIT_HEALTH',
'UNIT_MAXHEALTH',
},
create = function(frame)
-- create health bar
local bar = CreateFrame("StatusBar", nil, frame)
bar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
bar:SetStatusBarColor(1, .8, .2, 1)
bar:SetPoint("TOPLEFT", 1, -1)
bar:SetPoint("BOTTOMRIGHT", -1, 1)
bar.bg = bar:CreateTexture(nil, "BACKGROUND")
bar.bg:SetTexture(0, 0, 0, 1)
bar.bg:SetAllPoints()
frame.bar = bar
end,
update = function(frame, event)
-- ignore empty or unrelated events
if not event then return end
if (event == "UNIT_HEALTH" or event == "UNIT_MAXHEALTH")
and arg1 ~= frame.unitstr then return end
-- update statusbar values
frame.bar:SetMinMaxValues(0, UnitHealthMax(frame.unitstr))
frame.bar:SetValue(UnitHealth(frame.unitstr))
-- update health bar color
local r, g, b = .8, .8, .8
local _, class = UnitClass(frame.unitstr)
if class and RAID_CLASS_COLORS[class] then
r, g, b = RAID_CLASS_COLORS[class].r, RAID_CLASS_COLORS[class].g, RAID_CLASS_COLORS[class].b
end
frame.bar:SetStatusBarColor(r, g, b, 1)
end
})
UnitFrame_NewComponent('mana', {
events = {
'UNIT_MANA',
'UNIT_MAXMANA',
'UNIT_ENERGY',
'UNIT_MAXENERGY',
'UNIT_RAGE',
'UNIT_MAXRAGE',
'UNIT_DISPLAYPOWER',
},
create = function(frame)
-- create mana bar
local height = frame:GetHeight()
frame.mana = CreateFrame("StatusBar", nil, frame.bar)
frame.mana:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
frame.mana:SetStatusBarColor(.2, .2, 1, 1)
frame.mana:SetPoint("TOPLEFT", 0, -height+5)
frame.mana:SetPoint("BOTTOMRIGHT", 0, 0)
frame.mana.bg = frame.mana:CreateTexture(nil, "BACKGROUND")
frame.mana.bg:SetTexture(0, 0, 0, 1)
frame.mana.bg:SetAllPoints()
end,
update = function(frame, event)
-- ignore empty or unrelated events
if not event then return end
if (event == "UNIT_MANA" or event == "UNIT_MAXMANA"
or event == "UNIT_ENERGY" or event == "UNIT_MAXENERGY"
or event == "UNIT_RAGE" or event == "UNIT_MAXRAGE"
or event == "UNIT_DISPLAYPOWER")
and arg1 ~= frame.unitstr then return end
-- update mana bar values
frame.mana:SetMinMaxValues(0, UnitManaMax(frame.unitstr))
frame.mana:SetValue(UnitMana(frame.unitstr))
-- update mana bar colors
local color = ManaBarColor[UnitPowerType(frame.unitstr)] or { r = .2, g = .2, b = 1 }
frame.mana:SetStatusBarColor(color.r, color.g, color.b, 1)
end
})
UnitFrame_NewComponent('text', {
events = {
'FRAME_TICK_250',
},
create = function(frame)
-- create caption text
local text = frame.mana:CreateFontString(nil, "HIGH", "GameFontWhite")
text:SetAllPoints(frame.bar)
text:SetFont(UNIT_NAME_FONT, 8, "THINOUTLINE")
text:SetJustifyH("CENTER")
frame.text = text
end,
update = function(frame, event)
-- update caption text
local name = UnitName(frame.unitstr) or ""
local info = (not frame.compact and frame.info) and "\n"..frame.info or ""
frame.text:SetText(name .. info)
frame.info = nil
if frame.compact then return end
-- update info states
if not UnitIsConnected(frame.unitstr) then
frame.info = "|cffaaaaaaOffline|r"
end
if UnitIsDead(frame.unitstr) then
frame.info = "|cffaaaaaaDead|r"
end
if UnitIsDeadOrGhost(frame.unitstr) and UnitHealth(frame.unitstr) == 1 then
frame.info = "|cffaaaaaaGhost|r"
end
end
})
UnitFrame_NewComponent('border', {
events = { },
create = function(frame)
-- create border
local border = CreateFrame("Frame", nil, frame)
border:SetBackdrop(backdrop.border)
border:SetBackdropBorderColor(.8, .8, .8, 1)
border:SetPoint("TOPLEFT", frame, "TOPLEFT", -2,2)
border:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 2,-2)
border:SetFrameLevel(32)
ShaguTweaks.DarkenFrame(border)
frame.border = border
end,
update = function(frame, event)
end
})
UnitFrame_NewComponent('highlight', {
events = {
'FRAME_TICK_250',
},
create = function(frame)
-- create highlight border
local highlight = CreateFrame("Frame", nil, frame)
highlight:SetFrameLevel(128)
highlight:SetBackdrop(backdrop.border)
highlight:SetPoint("TOPLEFT", frame, "TOPLEFT", -1.5,1.5)
highlight:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 1.5,-1.5)
highlight:Hide()
frame.highlight = highlight
end,
update = function(frame, event)
if not event then return end
-- update health bar border
if UnitAffectingCombat(frame.unitstr) then
frame.highlight:SetBackdropBorderColor(.9, .1, .1, .5)
frame.highlight:Show()
else
frame.highlight:Hide()
end
end
})
UnitFrame_NewComponent('target', {
events = {
"PLAYER_TARGET_CHANGED",
},
create = function(frame)
-- create target marker
local target = CreateFrame("Frame", nil, frame.bar)
target:SetAllPoints()
local left = target:CreateTexture(nil, 'OVERLAY')
left:SetTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Up")
left:SetPoint("LEFT", target, "LEFT", 0, 0)
left:SetTexCoord(.3, .7, .3, .7)
left:SetDesaturated(true)
left:SetWidth(8)
left:SetHeight(8)
left:SetBlendMode('ADD')
local right = target:CreateTexture(nil, 'OVERLAY')
right:SetTexture("Interface\\Buttons\\UI-SpellbookIcon-PrevPage-Up")
right:SetPoint("RIGHT", target, "RIGHT", 1, 0)
right:SetTexCoord(.3, .7, .3, .7)
right:SetDesaturated(true)
right:SetWidth(8)
right:SetHeight(8)
right:SetBlendMode('ADD')
target:Hide()
frame.target = target
end,
update = function(frame, event)
if not event then return end
-- update health bar border
if UnitIsUnit("target", frame.unitstr) then
frame.target:Show()
frame.target:SetAlpha(1)
else
frame.target:Hide()
end
end
})
UnitFrame_NewComponent('icon', {
events = {
"RAID_TARGET_UPDATE",
},
create = function(frame)
-- create raid icon textures
frame.icon = frame.mana:CreateTexture(nil, "NORMAL")
frame.icon:SetWidth(12)
frame.icon:SetHeight(12)
frame.icon:SetPoint("BOTTOM", frame, "BOTTOM", 0, 1)
frame.icon:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcons")
frame.icon:Hide()
end,
update = function(frame, event)
if not event then return end
-- show raid icon if existing
if UnitName(frame.unitstr) and GetRaidTargetIndex(frame.unitstr) then
SetRaidTargetIconTexture(frame.icon, GetRaidTargetIndex(frame.unitstr))
frame.icon:Show()
else
frame.icon:Hide()
end
end
})
UnitFrame_NewComponent('range', {
events = {
'FRAME_TICK_250',
},
create = function(frame)
-- noop
end,
update = function(frame, event)
if not event then return end
-- range indicator
if UnitInRange(frame.unitstr) then
frame:SetAlpha(1)
else
frame:SetAlpha(.25)
end
end
})
ShaguTweaks.UnitFrame_NewComponent = UnitFrame_NewComponent
ShaguTweaks.UnitFrame_Refresh = UnitFrame_Refresh
-- Bring everything to life
module.enable = function(self)
local raid = CreateFrame("Frame", "ShaguTweaksRaidFrame", UIParent)
raid:RegisterEvent("RAID_ROSTER_UPDATE")
raid:RegisterEvent("PLAYER_ENTERING_WORLD")
raid:SetScript("OnEvent", function()
-- create all raid frames
if not raid.cluster.frames then
raid.cluster.frames = {}
for id = 1, 40 do CreateUnitFrame(raid.cluster, id) end
end
-- check for raid and setup frames
if UnitInRaid("player") then
for index = 1, 40 do
this.cluster.frames[index].unitstr = nil
this.cluster.frames[index]:Hide()
end
-- initialize raid frame
local x, y = 0, 0
for group = 1, 8 do
if RAID_SUBGROUP_LISTS and RAID_SUBGROUP_LISTS[group] then
for id, unit in ipairs(RAID_SUBGROUP_LISTS[group]) do
-- assign proper unitstrs to frames
local index = (group - 1) * 5 + id
local frame = this.cluster.frames[index]
frame.unitstr = 'raid' .. unit
frame.groupid = group
frame:Show()
UnitFrame_Refresh(frame)
-- save required raid frame size
x = math.max(x, frame.left)
y = math.max(y, frame.top)
end
end
end
-- set raid frame size
raid.cluster:SetWidth(x * (raid.cluster.config["raid.width"]+2) + 6)
raid.cluster:SetHeight(y * (raid.cluster.config["raid.height"]+1) + 7)
raid:Show()
else
raid:Hide()
end
end)
-- assign defaul scale
raid:SetScale(module.config["raid.scale"])
do -- toggle button
-- create toggle button
raid.toggle = CreateFrame("Button", "ShaguTweaksRaidToggle", raid)
raid.toggle:SetPoint("LEFT", UIParent, "LEFT", -8, 64)
raid.toggle:SetWidth(16)
raid.toggle:SetHeight(64)
raid.toggle:SetBackdrop(backdrop.background)
raid.toggle:SetBackdropColor(.2,.2,.2,1)
raid.toggle:SetBackdropBorderColor(.8,.8,.8,1)
ShaguTweaks.DarkenFrame(raid.toggle)
-- make button movable
raid.toggle:EnableMouse(true)
raid.toggle:SetMovable(true)
raid.toggle:SetUserPlaced(true)
raid.toggle:SetScript("OnEnter", function()
GameTooltip_SetDefaultAnchor(GameTooltip, this)
GameTooltip:ClearLines()
GameTooltip:AddLine(T["Raid Frames"], 1, 1, 1)
GameTooltip:AddDoubleLine(T["Click"], T["Toggle Raid Frames"])
GameTooltip:AddDoubleLine(T["Shift-Drag"], T["Move Raid Frames"])
GameTooltip:Show()
end)
raid.toggle:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
-- show icon on toggle button
raid.toggle.icon = raid.toggle:CreateTexture(nil, 'OVERLAY')
raid.toggle.icon:SetTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Up")
raid.toggle.icon:SetPoint("CENTER", raid.toggle, "CENTER", 2, 0)
raid.toggle.icon:SetTexCoord(.3, .7, .3, .7)
raid.toggle.icon:SetWidth(8)
raid.toggle.icon:SetHeight(8)
raid.toggle.icon:SetBlendMode('ADD')
-- texture mouse hover effect
raid.toggle:SetHighlightTexture("Interface\\Buttons\\UI-CheckBox-Highlight")
local pushed = raid.toggle:GetHighlightTexture()
pushed:ClearAllPoints()
pushed:SetPoint("CENTER", raid.toggle, "CENTER", 0, -2)
pushed:SetWidth(20)
pushed:SetHeight(110)
pushed:SetAlpha(.5)
local function UpdateToggleDrag()
local px, py = GetCursorPosition()
local scale = raid.toggle:GetEffectiveScale()
px, py = px / scale, py / scale
raid.toggle:ClearAllPoints()
raid.toggle:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", -8, py-32)
end
raid.toggle:SetScript("OnMouseDown", function()
local shift = API.IsShiftKeyDown()
if shift then
this.dragging = true
this:SetScript("OnUpdate", UpdateToggleDrag)
UpdateToggleDrag()
else
if raid.cluster:IsShown() then
raid.toggle.icon:SetTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Disabled")
raid.cluster:Hide()
else
raid.toggle.icon:SetTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Up")
raid.cluster:Show()
end
end
end)
raid.toggle:SetScript("OnMouseUp", function()
this.dragging = false
this:SetScript("OnUpdate", nil)
end)
end
do -- cluster
-- create raid cluster
raid.cluster = CreateFrame("Frame", "ShaguTweaksRaidCluster", raid)
raid.cluster:SetBackdrop(backdrop.background)
raid.cluster:SetBackdropColor(.2,.2,.2,1)
raid.cluster:SetBackdropBorderColor(.8,.8,.8,1)
raid.cluster:SetPoint("LEFT", raid.toggle, "RIGHT", -2, 0)
raid.cluster:SetClampedToScreen(true)
ShaguTweaks.DarkenFrame(raid.cluster)
-- assign default config
raid.cluster.config = module.config
end
do -- shared 250ms component ticker
raid.ticker = CreateFrame("Frame", nil, raid)
raid.ticker:SetScript("OnUpdate", function()
this.elapsed = (this.elapsed or 0) + (arg1 or 0)
if this.elapsed < .250 then return end
this.elapsed = this.elapsed - .250
if not raid.cluster.frames then return end
for _, frame in ipairs(raid.cluster.frames) do
if frame:IsVisible() and frame.unitstr and UnitName(frame.unitstr) then
local tickers = frame.events['FRAME_TICK_250']
if tickers then
for _, component in ipairs(tickers) do
component.update(frame, 'FRAME_TICK_250')
end
end
end
end
end)
end
NotifyRaidFrameReady(raid)
end