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