Files
OmicronFrames/src/types/unitframe.lua

421 lines
12 KiB
Lua

-- Copyright 2023 <omicron.me@protonmail.com>
--
-- 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 <https://www.gnu.org/licenses/>.
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
]]--