-- Copyright 2023 -- -- This file is part of Omicron Frames -- -- Omicron Frames is free software: you can redistribute it and/or modify it -- under the terms of the GNU General Public License as published by the Free -- Software Foundation, either version 3 of the License, or (at your option) -- any later version. -- -- Omicron Frames is distributed in the hope that it will be useful, but -- WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY -- or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -- more details. -- -- You should have received a copy of the GNU General Public License along with -- Omicron Frames. If not, see . local omif = select(2, ...) local types = omif.GetModule("types") local StatusBar = types.StatusBar local AuraList = types.AuraList local Indicator = types.Indicator types.UnitFrame = types.CreateClass("UnitFrame") local UnitFrame = types.UnitFrame local colors = { hostile = {0.5, 0.0, 0.0}, neutral = {0.7, 0.7, 0.0}, healthy = {0, 0.1, 0}, high = {0, 1.0, 0}, mid = {1.0, 1.0, 0}, low = {1.0, 0.0, 0}, offline = {0.7, 0.7, 0.7}, dead = {0.7, 0.7, 0.7}, magic = {0.4, 0.4, 1.0}, disease = {0.4, 0.2, 0.0}, poison = {0.0, 0.7, 0.7}, curse = {0.7, 0.0, 0.7}, immune = {0.0, 0.2, 0.4}, bomb = {1.0, 0.7, 0.7}, cyan = {0.0, 0.8, 0.8}, white = {1.0, 1.0, 1.0} } function UnitFrame:Init(unit, width, height, hideInRaid) self.unit = unit self.hideInRaid = hideInRaid or false local secure = self:CreateSecureFrame(width, height) secure:Hide() self.hp = StatusBar:new(self, width, height) self.auras = AuraList:new(self) local overlays = CreateFrame("Frame", nil, secure) overlays:SetFrameStrata("MEDIUM") overlays:SetFrameLevel(100) overlays:SetAllPoints(true) overlays:Show() self.overlays = overlays self:CreateName() self.indicators = { Indicator:new(self, self.auras, 14, "TOPLEFT", 2, -2, colors.white, 383648), Indicator:new(self, self.auras, 14, "TOPLEFT", 2, -2, colors.white, 974), Indicator:new(self, self.auras, 14, "BOTTOMLEFT", 2, 2, colors.cyan, 61295) } self:RegisterEvents() self:Enable() end function UnitFrame:StartRangeTicker() if self.rangeTicker then return end local delta = 0.45 + (fastrandom(0, 100)/1000) self.rangeTicker = C_Timer.NewTicker(delta, function() self:UpdateRange() end) end -- TODO: maybe string indicators need to be a thing function UnitFrame:CreateName() local name = self.overlays:CreateFontString(nil, "BACKGROUND") self.name = name name:SetFont("Interface\\AddOns\\OmicronFrames\\media\\fonts\\roboto\\Roboto-Bold.ttf", 13, "") name:SetTextColor(0.8, 0.6, 0.1) name:SetShadowColor(0, 0, 0) name:SetShadowOffset(1, -1) name:SetPoint("CENTER") name:SetText("") name:Show() end function UnitFrame:StopRangeTicker() if self.rangeTicker then self.rangeTicker:Cancel() self.rangeTicker = nil end end function UnitFrame:UpdateRange() local unit = self.unit if UnitIsDead(unit) then self.secureFrame:SetAlpha(1.0) elseif not UnitIsFriend("player", unit) and IsSpellInRange("Lightning Bolt", unit) ~= 1 then self.secureFrame:SetAlpha(0.2) elseif UnitIsFriend("player", unit) and IsSpellInRange("Healing Surge", unit) ~= 1 then self.secureFrame:SetAlpha(0.2) else self.secureFrame:SetAlpha(1.0) end end function UnitFrame:SetSpellAction(button, spell) local secure = self.secureFrame local attributeName = button:gsub("type", "spell") secure:SetAttribute(button, "spell") secure:SetAttribute(attributeName, spell) end function UnitFrame:SetMacroAction(button, macro) local secure = self.secureFrame local attributeName = button:gsub("type", "macrotext") secure:SetAttribute(button, "macro") secure:SetAttribute(attributeName, macro:gsub("@UNIT", "@" .. self.unit)) end function UnitFrame:PrepareWheelBinds() -- By default you can't use the SecureUnitButtonTemplate to handle scroll -- wheel "clicks". We solve this by using SecureHandler*Template to bind -- scroll wheel to the current unit frame button when the mouse enters and -- to unbind it when the mouse leaves. When the keybind is triggered the -- button will receive a click with a custom button name local secure = self.secureFrame local bindScript = ([[ self:ClearBindings() self:SetBindingClick(false, "MOUSEWHEELUP", "BUTTONNAME", "wheel-up") self:SetBindingClick(false, "MOUSEWHEELDOWN", "BUTTONNAME", "wheel-down") self:SetBindingClick(false, "ALT-MOUSEWHEELUP", "BUTTONNAME", "alt-wheel-up") self:SetBindingClick(false, "ALT-MOUSEWHEELDOWN", "BUTTONNAME", "alt-wheel-down") ]]):gsub("BUTTONNAME", "OmicronSecureFrame" .. self.unit) local removeBindScript = [[ self:ClearBindings() ]] secure:SetAttribute("_onenter", bindScript) secure:SetAttribute("_onleave", removeBindScript) secure:SetAttribute("_onshow", removeBindScript) secure:SetAttribute("_onhide", removeBindScript) end function UnitFrame:CreateSecureFrame(width, height) local name = "OmicronSecureFrame" .. self.unit local templates = table.concat({ "SecureUnitButtonTemplate", "SecureHandlerEnterLeaveTemplate", "SecureHandlerShowHideTemplate", }, ",") local secure = CreateFrame("Button", name, UIParent, templates) self.secureFrame = secure secure:SetFrameStrata("MEDIUM") secure:SetFrameLevel(0) secure:SetAttribute("unit", self.unit) secure:SetSize(width, height) secure:RegisterForClicks("AnyDown") self:PrepareWheelBinds() -- No modifiers secure:SetAttribute("type1", "target") self:SetMacroAction("type2", "/use [@UNIT,dead,help]Ancestral Vision; [@UNIT]Chain Heal") self:SetMacroAction("type3", "/use [@UNIT,dead,help]Ancestral Spirit; [@UNIT]Purify Spirit") self:SetMacroAction("type-wheel-up", "/use [@UNIT]Healing Surge") self:SetMacroAction("type-wheel-down", "/use [@UNIT,help]Riptide") -- alt self:SetSpellAction("type-alt-wheel-up", "Healing Wave") self:SetSpellAction("type-alt-wheel-down", "Earth Shield") -- Shift secure:SetAttribute("SHIFT-type2", "togglemenu") return secure end function UnitFrame:Enable() local secure = self.secureFrame if self.hideInRaid then local condition = "[@UNIT,exists,nogroup:raid] show; hide" condition = condition:gsub("@UNIT", "@" .. self.unit) RegisterAttributeDriver(secure, "state-visibility", condition) elseif self.unit ~= "player" then RegisterUnitWatch(self.secureFrame, false) else self.secureFrame:Show() end if UnitExists(self.unit) then self:UpdateAll(self:HasUnitChanged()) end end function UnitFrame:Disable() if self.hideInRaid then UnregisterAttributeDriver(self.secureFrame, "state-visibility") elseif self.unit ~= "player" then UnregisterUnitWatch(self.secure_frame, self.condition) else self.secureFrame:Hide() end end -- returns true if this unit is a target unit function UnitFrame:IsTargetFrame() return self.unit:match(".*target") == self.unit end -- returns the target owner function UnitFrame:GetTargetOwner() if self.unit == "target" then return "player" else return self.unit:sub(1, -7) end end -- Set position from the center of the UIParent function UnitFrame:SetPosition(x, y) self.secureFrame:SetPoint("CENTER", UIParent, x, y) end function UnitFrame:RegisterEvents() local unit = self.unit local secure = self.secureFrame secure:SetScript("OnShow", function(frame, event, ...) self:OnShow() end) secure:SetScript("OnHide", function(frame, event, ...) self:OnHide() end) secure:SetScript("OnEvent", function(frame, event, ...) self[event](self, ...) end) if self:IsTargetFrame() then local owner = self:GetTargetOwner() if owner == "player" then secure:RegisterEvent("PLAYER_TARGET_CHANGED") else secure:RegisterUnitEvent("UNIT_TARGET", self.parent:GetTargetOwner()) end end secure:RegisterUnitEvent("UNIT_AURA", unit) secure:RegisterUnitEvent("UNIT_HEALTH", unit) secure:RegisterUnitEvent("UNIT_MAXHEALTH", unit) secure:RegisterUnitEvent("UNIT_NAME_UPDATE", unit) end -- returns whether or not the unit guid has changed since the last call to this -- function. Always returns false if the current unit does not exist. function UnitFrame:HasUnitChanged() local unit = self.unit if not UnitExists(unit) then self.guid = nil return false end local guid = UnitGUID(unit) if self.guid == guid then return false else self.guid = guid return true end end function UnitFrame:UNIT_AURA(unit, info) if self:HasUnitChanged() then self:UpdateAll(true) return end if self.auras:Update(info) then self:UpdateHealthColor() end end function UnitFrame:UNIT_HEALTH() if self:HasUnitChanged() then self:UpdateAll(true) return end self:UpdateHealth() self:UpdateHealthColor() end function UnitFrame:UNIT_MAXHEALTH() if self:HasUnitChanged() then self:UpdateAll(true) return end self:UpdateHealth() self:UpdateHealthColor() end function UnitFrame:UNIT_TARGET() self:UpdateAll(self:HasUnitChanged()) end function UnitFrame:PLAYER_TARGET_CHANGED() self:UpdateAll(self:HasUnitChanged()) end function UnitFrame:UNIT_NAME_UPDATE() self:UpdateName() end function UnitFrame:OnShow() self:StartRangeTicker() self:UpdateAll(self:HasUnitChanged()) end function UnitFrame:OnHide() self.guid = nil self:StopRangeTicker() end function UnitFrame:UpdateAll(unitChanged) if not UnitExists(self.unit) then return end if unitChanged then self.auras:Reset() end self:UpdateHealth() self:UpdateHealthColor() self:UpdateRange() self:UpdateName() end function UnitFrame:UpdateHealth() local unit = self.unit local val = UnitHealth(unit) local max = UnitHealthMax(unit) self.hp:SetRange(0, max) self.hp:SetValue(val) end function UnitFrame:UpdateHealthColor() local unit = self.unit local val = UnitHealth(unit) local max = UnitHealthMax(unit) if max == 0 then return end local pct = val / max local isFriend = UnitIsFriend("player", unit) if not isFriend then self.hp:SetColor(unpack(colors.hostile)) elseif self.auras.statusCount["Immune"] then self.hp:SetColor(unpack(colors.immune)) elseif self.auras.statusCount["Bomb"] then self.hp:SetColor(unpack(colors.bomb)) elseif self.auras.statusCount["Magic"] then self.hp:SetColor(unpack(colors.magic)) elseif self.auras.statusCount["Curse"] then self.hp:SetColor(unpack(colors.curse)) elseif self.auras.statusCount["Poison"] then self.hp:SetColor(unpack(colors.poison)) elseif self.auras.statusCount["Disease"] then self.hp:SetColor(unpack(colors.disease)) elseif isFriend and UnitIsDead(unit) then self.hp:SetColor(unpack(colors.dead)) elseif isFriend and pct >= 0.90 then self.hp:SetColor(unpack(colors.healthy)) elseif isFriend and pct >= 0.75 then local progress = (pct - 0.75) / (0.90-0.75) self.hp:SetInterpolatedColor(colors.mid, colors.high, progress) else local progress = pct / 0.75 -- Quadratic progress so we get to the dangerous colors faster as hp -- goes lower progress = progress * progress self.hp:SetInterpolatedColor(colors.low, colors.mid, progress) end end function UnitFrame:UpdateName() if UnitIsPlayer(self.unit) then local _, class = UnitClass(self.unit) local color = RAID_CLASS_COLORS[class] self.name:SetTextColor(color.r, color.g, color.b) else self.name:SetTextColor(1, 1, 1) end self.name:SetText(UnitName(self.unit):sub(1, 5)) end --[[ -- UNIT_AURA -- UNIT_CLASSIFICATION_CHANGED -- UNIT_COMBAT -- UNIT_CONNECTION -- UNIT_DISPLAYPOWER -- UNIT_FACTION -- UNIT_FLAGS -- UNIT_LEVEL -- UNIT_MANA -- UNIT_HEALTH_PREDICTION -- UNIT_PHASE ]]--