if not Cursive.superwow then return end local L = AceLibrary("AceLocale-2.2"):new("Cursive") local utils = Cursive.utils local filter = Cursive.filter local ui = CreateFrame("Frame", "CursiveUI", UIParent) ui.border = { edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 8, insets = { left = 2, right = 2, top = 2, bottom = 2 } } ui.background = { bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", tile = true, tileSize = 16, edgeSize = 8, insets = { left = 0, right = 0, top = 0, bottom = 0 } } ui.rootBarFrame = nil ui.targetIndicatorSize = 8 ui.padding = 2 ui.row = 1 ui.col = 1 ui.maxBarsDisplayed = false ui.numDisplayed = 0 local function GetBarFirstSectionWidth() local config = Cursive.db.profile local size = 1 if config.showraidicons then size = size + config.raidiconsize end if config.showtargetindicator then size = size + ui.targetIndicatorSize end if size > 0 then size = size + ui.padding end return size end local function GetBarSecondSectionWidth() local config = Cursive.db.profile if config.showhealthbar == false and config.showunitname == false then return 1 end return config.healthwidth + ui.padding end local function GetBarThirdSectionWidth() local config = Cursive.db.profile return config.maxcurses * (config.curseiconsize + ui.padding) end local function GetBarWidth() return GetBarFirstSectionWidth() + GetBarSecondSectionWidth() + GetBarThirdSectionWidth() end local function UpdateRootBarFrame() local config = Cursive.db.profile if config.showbackdrop then ui.rootBarFrame:SetBackdrop(ui.background) else ui.rootBarFrame:SetBackdrop(nil) end ui.rootBarFrame:EnableMouse(not config.clickthrough) ui.rootBarFrame.pos = config.anchor .. config.x .. config.y .. config.scale ui.rootBarFrame:ClearAllPoints() ui.rootBarFrame:SetPoint(config.anchor, config.x, config.y) ui.rootBarFrame:SetScale(config.scale) ui.rootBarFrame.caption:SetFont(STANDARD_TEXT_FONT, Cursive.db.profile.textsize, "THINOUTLINE") ui.rootBarFrame.caption:SetText(Cursive.db.profile.caption) if Cursive.db.profile.showtitle then ui.rootBarFrame.caption:Show() else ui.rootBarFrame.caption:Hide() end ui.rootBarFrame:SetWidth(config.maxcol * GetBarWidth()) -- Calculate height: title area + all rows + extra spacing local title_size = 12 + config.spacing local total_height = title_size + (config.maxrow * (config.height + config.spacing)) + config.spacing ui.rootBarFrame:SetHeight(total_height) end local function CreateRoot() local frame = CreateFrame("Frame", Cursive.db.profile.caption, UIParent) ui.rootBarFrame = frame frame.id = Cursive.db.profile.caption frame:RegisterForDrag("LeftButton") frame:SetMovable(true) frame:SetScript("OnDragStart", function() this.lock = true this:StartMoving() end) frame:SetScript("OnDragStop", function() -- convert to best anchor depending on position local new_anchor = utils.GetBestAnchor(this) local anchor, x, y = utils.ConvertFrameAnchor(this, new_anchor) this:ClearAllPoints() this:SetPoint(anchor, UIParent, anchor, x, y) -- save new position anchor, _, _, x, y = this:GetPoint() Cursive.db.profile.anchor, Cursive.db.profile.x, Cursive.db.profile.y = anchor, x, y -- stop drag this:StopMovingOrSizing() this.lock = false this:ClearAllPoints() this:SetPoint(anchor, x, y) end) -- create title text frame.caption = frame:CreateFontString(nil, "HIGH", "GameFontWhite") frame.caption:SetPoint("TOPLEFT", frame, "TOPLEFT", 10, -2) frame.caption:SetTextColor(1, 1, 1, 1) UpdateRootBarFrame() frame:Show() return frame end ui.unitFrames = {} -- holds all unitFrames for all columns/rows Cursive.UpdateFramesFromConfig = function() for col, rows in pairs(ui.unitFrames) do for row, unitFrame in pairs(rows) do if unitFrame and unitFrame:IsShown() then unitFrame:Hide() end end end if ui.rootBarFrame then UpdateRootBarFrame() end -- after 3 seconds reset the unit frames so all changes are applied Cursive:ScheduleEvent("resetUnitFrames", Cursive.ResetUnitFrames, 3) end Cursive.ResetUnitFrames = function() -- hide all existing unit frames for col, rows in pairs(ui.unitFrames) do for row, unitFrame in pairs(rows) do if unitFrame and unitFrame:IsShown() then unitFrame:Hide() end end end -- clear cached frames so they are recreated ui.unitFrames = {} end ui.BarEnter = function() if this.parent.healthBar then this.parent.healthBar.border:SetBackdropBorderColor(1, 1, 1, 1) end this.parent.hover = true GameTooltip_SetDefaultAnchor(GameTooltip, this) GameTooltip:SetUnit(this.parent.guid) GameTooltip:Show() end ui.BarLeave = function() this.parent.hover = false GameTooltip:Hide() end ui.BarUpdate = function() if not this.guid or this.guid == 0 then this:Hide() return end if (this.tick or 1) > GetTime() then return else this.tick = GetTime() + 0.05 end -- update statusbar values if it exists if this.healthBar then this.healthBar:SetMinMaxValues(0, UnitHealthMax(this.guid)) this.healthBar:SetValue(UnitHealth(this.guid)) -- update health bar color local hex, r, g, b, a = utils.GetUnitColor(this.guid) this.healthBar:SetStatusBarColor(r, g, b, a) -- update health bar border if this.healthBar.border then if this.hover then this.healthBar.border:SetBackdropBorderColor(1, 1, 1, 1) elseif UnitAffectingCombat(this.guid) then this.healthBar.border:SetBackdropBorderColor(.8, .2, .2, 1) else this.healthBar.border:SetBackdropBorderColor(.2, .2, .2, 1) end end end -- update caption text local name = UnitName(this.guid) if name and this.nameText then this.nameText:SetText(name) end if this.hpText then local hp = UnitHealth(this.guid) if GetLocale() == "zhCN" then if hp then if hp >= 10000 then hp = math.floor(hp / 1000) / 10 .. "δΈ‡" -- elseif hp >= 1000 then -- hp = math.floor(hp / 100) / 10 .. "k" end end else -- convert hp to k if > 1000 if hp then if hp >= 1000000 then hp = math.floor(hp / 100000) / 10 .. "m" elseif hp >= 1000 then hp = math.floor(hp / 100) / 10 .. "k" end end end if hp then this.hpText:SetText(hp) end end -- show raid icon if existing if this.icon then if GetRaidTargetIndex(this.guid) and Cursive.filter.alive(this.guid) then SetRaidTargetIconTexture(this.icon, GetRaidTargetIndex(this.guid)) this.icon:Show() else this.icon:Hide() end end -- update target indicator if this.target_left then if UnitIsUnit("target", this.guid) then this.target_left:Show() else this.target_left:Hide() end end end ui.BarClick = function() if arg1 == "LeftButton" then TargetUnit(this.parent.guid) elseif arg1 == "RightButton" then TargetUnit(this.parent.guid) if (not PlayerFrame.inCombat) then AttackTarget() end end end local function CreateBarFirstSection(unitFrame, guid) local config = Cursive.db.profile local firstSection = CreateFrame("Frame", "Cursive1stSection", unitFrame) if config.invertbars then -- When inverted, position relative to second section (rightmost) firstSection:SetPoint("LEFT", unitFrame.secondSection, "RIGHT", 0, 0) else -- Normal positioning (leftmost) firstSection:SetPoint("LEFT", unitFrame, "LEFT", 0, 0) end firstSection:SetWidth(GetBarFirstSectionWidth()) firstSection:SetHeight(config.height) firstSection:EnableMouse(false) unitFrame.firstSection = firstSection -- create target indicator if config.showtargetindicator then local targetLeft = firstSection:CreateTexture(nil, "OVERLAY") targetLeft:SetWidth(ui.targetIndicatorSize) targetLeft:SetHeight(8) if config.invertbars then targetLeft:SetPoint("RIGHT", firstSection, "RIGHT", 0, 0) targetLeft:SetTexture("Interface\\AddOns\\Cursive\\img\\target-right") else targetLeft:SetPoint("LEFT", unitFrame, "LEFT", 0, 0) targetLeft:SetTexture("Interface\\AddOns\\Cursive\\img\\target-left") end targetLeft:Hide() unitFrame.target_left = targetLeft end -- create raid icon textures if config.showraidicons then local icon = firstSection:CreateTexture(nil, "OVERLAY") icon:SetWidth(config.raidiconsize) icon:SetHeight(config.raidiconsize) if config.invertbars then icon:SetPoint("LEFT", firstSection, "LEFT", 0, 0) else icon:SetPoint("RIGHT", firstSection, "RIGHT", 0, 0) end icon:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcons") icon:Hide() unitFrame.icon = icon end end local function CreateBarSecondSection(unitFrame, guid) local config = Cursive.db.profile local secondSection = CreateFrame("Button", "Cursive2ndSection", unitFrame) if config.invertbars then -- When inverted, position relative to third section (which is created first) secondSection:SetPoint("LEFT", unitFrame.thirdSection, "RIGHT", 0, 0) else -- Normal positioning relative to first section secondSection:SetPoint("LEFT", unitFrame.firstSection, "RIGHT", 0, 0) end secondSection:SetWidth(GetBarSecondSectionWidth()) secondSection:SetHeight(config.height) unitFrame.secondSection = secondSection secondSection.parent = unitFrame secondSection:RegisterForClicks("LeftButtonUp", "RightButtonUp") secondSection:SetScript("OnClick", ui.BarClick) secondSection:SetScript("OnEnter", ui.BarEnter) secondSection:SetScript("OnLeave", ui.BarLeave) -- create health bar if config.showhealthbar then local healthBar = CreateFrame("StatusBar", "CursiveHealthBar", secondSection) healthBar:SetStatusBarTexture(config.bartexture) healthBar:SetStatusBarColor(1, .8, .2, 1) healthBar:SetMinMaxValues(0, 100) healthBar:SetValue(20) healthBar:SetPoint("LEFT", secondSection, "LEFT", ui.padding, 0) healthBar:SetWidth(config.healthwidth) healthBar:SetHeight(config.height) unitFrame.healthBar = healthBar local hp = healthBar:CreateFontString(nil, "HIGH", "GameFontWhite") hp:SetPoint("TOPRIGHT", healthBar, "TOPRIGHT", -2, -2) hp:SetWidth(30) hp:SetHeight(config.height - 4) hp:SetFont(STANDARD_TEXT_FONT, config.textsize, "THINOUTLINE") hp:SetJustifyH("RIGHT") unitFrame.hpText = hp if config.showunitname then local name = healthBar:CreateFontString(nil, "HIGH", "GameFontWhite") name:SetPoint("TOPLEFT", healthBar, "TOPLEFT", 2, -2) name:SetPoint("BOTTOMRIGHT", hp, "BOTTOMLEFT", 2, 0) name:SetFont(STANDARD_TEXT_FONT, config.textsize, "THINOUTLINE") name:SetJustifyH("LEFT") unitFrame.nameText = name end -- create health bar backdrops if pfUI and pfUI.uf then pfUI.api.CreateBackdrop(healthBar) healthBar.border = healthBar.backdrop else healthBar:SetBackdrop(ui.background) healthBar:SetBackdropColor(0, 0, 0, 1) local border = CreateFrame("Frame", "CursiveBorder", healthBar.bar) border:SetBackdrop(ui.border) border:SetBackdropColor(.2, .2, .2, 1) border:SetPoint("TOPLEFT", healthBar.bar, "TOPLEFT", -2, 2) border:SetPoint("BOTTOMRIGHT", healthBar.bar, "BOTTOMRIGHT", 2, -2) healthBar.border = border end else if config.showunitname then local name = secondSection:CreateFontString(nil, "HIGH", "GameFontWhite") name:SetPoint("TOPLEFT", secondSection, "TOPLEFT", 2, -2) name:SetPoint("BOTTOMRIGHT", secondSection, "BOTTOMRIGHT", 2, 0) name:SetFont(STANDARD_TEXT_FONT, config.textsize, "THINOUTLINE") name:SetWidth(config.healthwidth) name:SetHeight(config.height - 4) name:SetJustifyH("LEFT") unitFrame.nameText = name end end end local function CreateBarThirdSection(unitFrame, guid) local config = Cursive.db.profile local thirdSection = CreateFrame("Frame", "Cursive3rdSection", unitFrame) if config.invertbars then -- When inverted, this is positioned first (leftmost) thirdSection:SetPoint("LEFT", unitFrame, "LEFT", 0, 0) else -- Normal positioning relative to second section thirdSection:SetPoint("LEFT", unitFrame.secondSection, "RIGHT", 0, 0) end thirdSection:SetWidth(GetBarThirdSectionWidth()) thirdSection:SetHeight(config.height) thirdSection:EnableMouse(false) unitFrame.thirdSection = thirdSection -- display up to maxcurses curses for i = 1, config.maxcurses do local curse = thirdSection:CreateTexture(nil, "OVERLAY") curse:SetWidth(config.curseiconsize) curse:SetHeight(config.curseiconsize) if config.invertbars then -- When inverted, position from right to left local rightOffset = i * ui.padding + ((i - 1) * config.curseiconsize) curse:SetPoint("RIGHT", thirdSection, "RIGHT", -rightOffset, 0) else -- Normal positioning from left to right curse:SetPoint("LEFT", thirdSection, "LEFT", i * ui.padding + ((i - 1) * config.curseiconsize), 0) end curse.timer = thirdSection:CreateFontString(nil, "OVERLAY", "GameFontNormal") curse.timer:SetFontObject(GameFontHighlight) curse.timer:SetFont(STANDARD_TEXT_FONT, config.cursetimersize, "OUTLINE") curse.timer:SetTextColor(1, 1, 1) curse.timer:SetAllPoints(curse) curse.timer:Hide() curse:Hide() unitFrame["curse" .. i] = curse end end local function CreateBar(row, col, guid) local unitFrame = CreateFrame("Frame", "CursiveUnitFrame", ui.rootBarFrame) unitFrame.guid = guid unitFrame:SetScript("OnUpdate", ui.BarUpdate) local config = Cursive.db.profile local width = GetBarWidth() unitFrame:SetWidth(width) unitFrame:SetHeight(config.height) local config = Cursive.db.profile if config.invertbars then -- Create sections in reverse order: 3 -> 2 -> 1 CreateBarThirdSection(unitFrame, guid) CreateBarSecondSection(unitFrame, guid) CreateBarFirstSection(unitFrame, guid) else -- Normal order: 1 -> 2 -> 3 CreateBarFirstSection(unitFrame, guid) CreateBarSecondSection(unitFrame, guid) CreateBarThirdSection(unitFrame, guid) end ui.unitFrames[col][row] = unitFrame return unitFrame end local function GetBarCords(row, col) local config = Cursive.db.profile local x = (col - 1) * GetBarWidth() local y if config.expandupwards then -- For upward expansion: start from bottom with spacing, then go up y = config.spacing + ((row - 1) * (config.height + config.spacing)) else -- For downward expansion: use original logic (don't subtract 1 to account for header) y = -(row * (config.height + config.spacing)) end return x, y end local function hasAnySpellId(guid, spellIds) for i = 1, 16 do local texture, stacks, spellSchool, spellId = UnitDebuff(guid, i); if not spellId then break end if spellIds[spellId] then return spellId end end for i = 1, 32 do local texture, stacks, spellId = UnitBuff(guid, i); if not spellId then break end if spellIds[spellId] then return spellId end end return nil end local function GetSortedCurses(guidCurses) -- Collect keys local curseNames = {} for key in pairs(guidCurses) do table.insert(curseNames, key) end if Cursive.db.profile.curseordering == L["Order applied"] then table.sort(curseNames, function(a, b) return guidCurses[a].start < guidCurses[b].start end) elseif Cursive.db.profile.curseordering == L["Expiring soonest -> latest"] then table.sort(curseNames, function(a, b) return Cursive.curses:TimeRemaining(guidCurses[a]) < Cursive.curses:TimeRemaining(guidCurses[b]) end) elseif Cursive.db.profile.curseordering == L["Expiring latest -> soonest"] then table.sort(curseNames, function(a, b) return Cursive.curses:TimeRemaining(guidCurses[a]) > Cursive.curses:TimeRemaining(guidCurses[b]) end) end local i = 0 return function() i = i + 1 local key = curseNames[i] if key then return key, guidCurses[key] end end end local function DisplayGuid(guid) if not ui.unitFrames[ui.col] then ui.unitFrames[ui.col] = {} end local unitFrame if ui.unitFrames[ui.col][ui.row] then unitFrame = ui.unitFrames[ui.col][ui.row] unitFrame.guid = guid else unitFrame = CreateBar(ui.row, ui.col, guid) ui.unitFrames[ui.col][ui.row] = unitFrame end local x, y = GetBarCords(ui.row, ui.col) -- update position if required local config = Cursive.db.profile if not unitFrame.pos or unitFrame.pos ~= x .. y then unitFrame:ClearAllPoints() if config.expandupwards then unitFrame:SetPoint("BOTTOMLEFT", ui.rootBarFrame, "BOTTOMLEFT", x, y) else unitFrame:SetPoint("TOPLEFT", ui.rootBarFrame, "TOPLEFT", x, y) end unitFrame.pos = x .. y end -- check for shared debuffs for sharedDebuffKey, guids in pairs(Cursive.curses.sharedDebuffGuids) do if guids[guid] then local sharedDebuffSpellIds = Cursive.curses.sharedDebuffs[sharedDebuffKey] local spellId = hasAnySpellId(guid, sharedDebuffSpellIds) if spellId ~= nil then -- add curse to curses Cursive.curses:ApplySharedCurse(sharedDebuffKey, spellId, guid, GetTime()) -- remove guid Cursive.curses.sharedDebuffGuids[sharedDebuffKey][guid] = nil end end end -- update curses local curseNumber = 1 -- make sure old curses are hidden for i = 1, Cursive.db.profile.maxcurses do local curse = unitFrame["curse" .. i] curse:Hide() curse.timer:Hide() end local guidCurses = Cursive.curses.guids[guid] if guidCurses then for curseName, curseData in GetSortedCurses(guidCurses) do if curseNumber > Cursive.db.profile.maxcurses then break end local remaining = Cursive.curses:TimeRemaining(curseData) local curse = unitFrame["curse" .. curseNumber] if remaining >= 0 then curse:SetTexture(Cursive.curses.trackedCurseIds[curseData.spellID].texture) if curseData["currentPlayer"] == false then curse:SetDesaturated(true); -- desaturate if not applied by current player else curse:SetDesaturated(false); -- saturate if applied by current player end -- curse:SetTexCoord(.078, .92, .079, .937) rounded icons curse.timer:SetText(remaining) curse.timer:Show() curse:Show() if remaining < 1 then if Cursive.curses:ShouldPlayExpiringSound(curseName, guid) then PlaySoundFile("Interface\\AddOns\\Cursive\\sounds\\expiring.mp3") end elseif Cursive.curses:HasRequestedExpiringSound(curseName, guid) then Cursive.curses:EnableExpiringSound(curseName, guid) end curseNumber = curseNumber + 1 end end end unitFrame:Show() ui.numDisplayed = ui.numDisplayed + 1 local config = Cursive.db.profile -- update row/col ui.row = ui.row + 1 if ui.row > config.maxrow then ui.row = 1 ui.col = ui.col + 1 if ui.col > config.maxcol then ui.maxBarsDisplayed = true end end end local function CheckForCleanup(guid, time) local active = UnitExists(guid) and Cursive.filter.alive(guid) if active then local old = GetTime() - time >= 900 -- >= 15 minutes old if old and not UnitIsVisible(guid) then active = false end end if not active then -- remove from core Cursive.core.remove(guid) -- remove from curses Cursive.curses:RemoveGuid(guid) -- remove from sharedDebuffGuids for sharedDebuffKey, guids in pairs(Cursive.curses.sharedDebuffGuids) do if guids[guid] then Cursive.curses.sharedDebuffGuids[sharedDebuffKey][guid] = nil end end end end local shouldDisplayGuids = {}; local displayedGuids = {}; ui:SetAllPoints() ui:SetScript("OnUpdate", function() local config = Cursive.db.profile if not config.enabled then return end if (this.tick or 1) > GetTime() then return else this.tick = GetTime() + 0.1 end if not ui.rootBarFrame then ui.rootBarFrame = CreateRoot() end -- skip if locked (due to moving) if ui.rootBarFrame.lock then return end -- reset display data ui.row = 1 ui.col = 1 ui.maxBarsDisplayed = false ui.numDisplayed = 0 -- clear shouldDisplayGuids for guid, _ in pairs(shouldDisplayGuids) do shouldDisplayGuids[guid] = nil end -- clear displayedGuids for guid, _ in pairs(displayedGuids) do displayedGuids[guid] = nil end -- run through all guids and fill with bars local title_size = 12 + config.spacing local topMaxHp = 0 local secondMaxHp = 0 local thirdMaxHp = 0 local topMaxGuid = 0 local secondMaxGuid = 0 local thirdMaxGuid = 0 local numDisplayable = 0 local averageMaxHp = 0 local _, currentTargetGuid = UnitExists("target") -- first consider raid marks for i = 8, 1, -1 do local _, guid = UnitExists("mark" .. i) if guid then if Cursive:ShouldDisplayGuid(guid) then numDisplayable = numDisplayable + 1 -- display guid displayedGuids[guid] = true DisplayGuid(guid) if ui.maxBarsDisplayed then break end end -- don't try to display this guid again shouldDisplayGuids[guid] = false end end for guid, time in pairs(Cursive.core.guids) do -- calculate shouldDisplay local shouldDisplay = false if shouldDisplayGuids[guid] == nil then shouldDisplay = Cursive:ShouldDisplayGuid(guid) shouldDisplayGuids[guid] = shouldDisplay if shouldDisplay then numDisplayable = numDisplayable + 1 end else shouldDisplay = shouldDisplayGuids[guid] end -- calculate top 3 max hps if shouldDisplay then local maxHp = UnitHealthMax(guid) if maxHp > topMaxHp then thirdMaxHp = secondMaxHp thirdMaxGuid = secondMaxGuid secondMaxHp = topMaxHp secondMaxGuid = topMaxGuid topMaxHp = maxHp topMaxGuid = guid elseif maxHp > secondMaxHp then thirdMaxHp = secondMaxHp thirdMaxGuid = secondMaxGuid secondMaxHp = maxHp secondMaxGuid = guid elseif maxHp > thirdMaxHp then thirdMaxHp = maxHp thirdMaxGuid = guid end else CheckForCleanup(guid, time) end end -- top max hp if not ui.maxBarsDisplayed and numDisplayable > ui.numDisplayed and not displayedGuids[topMaxGuid] then displayedGuids[topMaxGuid] = true DisplayGuid(topMaxGuid) end -- second max hp if not ui.maxBarsDisplayed and numDisplayable > ui.numDisplayed and not displayedGuids[secondMaxGuid] then displayedGuids[secondMaxGuid] = true DisplayGuid(secondMaxGuid) end -- third max hp if not ui.maxBarsDisplayed and numDisplayable > ui.numDisplayed and not displayedGuids[thirdMaxGuid] then displayedGuids[thirdMaxGuid] = true DisplayGuid(thirdMaxGuid) end -- fill in remaining slots for guid, time in pairs(Cursive.core.guids) do if ui.maxBarsDisplayed or numDisplayable <= ui.numDisplayed then break end if not displayedGuids[guid] and shouldDisplayGuids[guid] == true then displayedGuids[guid] = true DisplayGuid(guid) end end -- if current target not yet displayed, show it at maxrow/maxcol if currentTargetGuid and shouldDisplayGuids[currentTargetGuid] and not displayedGuids[currentTargetGuid] and Cursive.db.profile.alwaysshowcurrenttarget then -- replace the last displayed guid with the current target displayedGuids[currentTargetGuid] = true ui.col = config.maxcol ui.row = config.maxrow DisplayGuid(currentTargetGuid) end -- hide any remaining unit frames for col, rows in pairs(ui.unitFrames) do for row, unitFrame in pairs(rows) do if unitFrame:IsShown() then if not displayedGuids[unitFrame.guid] then unitFrame:Hide() else displayedGuids[unitFrame.guid] = nil -- avoid displaying duplicate rows end end end end end) Cursive.ui = ui