StatusTrigger will trigger if a status activates (or reaches a certain activation # of auras). This is an abstraction provided by AuraList which keeps track of which auras belong to which statuses. Added type annotations to all touched files
613 lines
17 KiB
Lua
613 lines
17 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")
|
|
|
|
---@class StatusBar
|
|
local StatusBar = types.StatusBar
|
|
|
|
---@class AuraList
|
|
local AuraList = types.AuraList
|
|
|
|
---@class SquareIndicator
|
|
local SquareIndicator = types.SquareIndicator
|
|
|
|
---@class AuraTrigger
|
|
local AuraTrigger = types.AuraTrigger
|
|
|
|
---@class StatusTrigger
|
|
local StatusTrigger = types.StatusTrigger
|
|
|
|
---@class UnitFrame
|
|
local UnitFrame = types.CreateClass("UnitFrame")
|
|
types.UnitFrame = 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}
|
|
}
|
|
|
|
---@param unit string
|
|
---@param config table
|
|
function UnitFrame:Init(unit, config)
|
|
local width = config.size.width
|
|
local height = config.size.height
|
|
|
|
self.unit = unit
|
|
self.hideInRaid = config.hideInRaid
|
|
|
|
self.rangeFriendly = config.range.friendly
|
|
self.rangeEnemy = config.range.enemy
|
|
self.rangeFade = config.range.fade
|
|
|
|
local secure = self:CreateSecureFrame(width, height)
|
|
secure:Hide()
|
|
|
|
self:SetMouseBinds(config.mouse)
|
|
|
|
self.hp = StatusBar:new(self, width, height, 0, true)
|
|
self.power = StatusBar:new(self, width, 6, 2, false)
|
|
self.power:Hide()
|
|
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:CreateTriggers(config.triggers)
|
|
|
|
self:RegisterEvents()
|
|
self:Enable()
|
|
end
|
|
|
|
function UnitFrame:CreateTriggers(triggers)
|
|
for _, trigger in ipairs(triggers) do
|
|
local kind = trigger.kind
|
|
local indicator = self:CreateIndicator(trigger.indicator)
|
|
if kind == "AuraTrigger" then
|
|
local at = AuraTrigger:new(
|
|
trigger.spellId,
|
|
trigger.own,
|
|
trigger.requiredCount,
|
|
trigger.invert
|
|
)
|
|
at:SetTarget(indicator)
|
|
self.auras:AddTrigger(at)
|
|
elseif kind == "StatusTrigger" then
|
|
local st = StatusTrigger:new(trigger.status, trigger.requiredCount, trigger.invert)
|
|
st:SetTarget(indicator)
|
|
self.auras:AddTrigger(st)
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
function UnitFrame:CreateIndicator(indicator)
|
|
local kind = indicator.kind
|
|
if kind == "SquareIndicator" then
|
|
return SquareIndicator:new(
|
|
self,
|
|
indicator.size,
|
|
indicator.point,
|
|
indicator.x,
|
|
indicator.y,
|
|
indicator.color
|
|
)
|
|
else
|
|
error(string.format("Invalid Indicator kind `%s` requested", indicator.kind))
|
|
end
|
|
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
|
|
|
|
-- Returns
|
|
-- true if the unit is in range
|
|
-- false if the unit is not in range
|
|
-- nil if it could not be determined
|
|
function UnitFrame:IsInRange()
|
|
local unit = self.unit
|
|
local friendlySpell = self.rangeFriendly
|
|
local enemySpell = self.rangeEnemy
|
|
|
|
-- Prefer to use configured spells
|
|
if friendlySpell and UnitCanAssist("player", unit) then
|
|
return IsSpellInRange(friendlySpell, unit) == 1
|
|
elseif enemySpell and UnitCanAttack("player", unit) then
|
|
return IsSpellInRange(enemySpell, unit) == 1
|
|
end
|
|
|
|
-- Fall back to raid/party only range check
|
|
local inRange, checkedRange = UnitInRange(unit)
|
|
if checkedRange then
|
|
return inRange
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
function UnitFrame:UpdateRange()
|
|
local unit = self.unit
|
|
|
|
if UnitIsDeadOrGhost(unit) then
|
|
self.secureFrame:SetAlpha(1.0)
|
|
return
|
|
end
|
|
|
|
local inRange = self:IsInRange()
|
|
if inRange or inRange == nil then
|
|
self.secureFrame:SetAlpha(1.0)
|
|
else
|
|
self.secureFrame:SetAlpha(self.rangeFade)
|
|
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
|
|
|
|
local function ModifiersToPrefix(mods)
|
|
if not mods then
|
|
return ""
|
|
end
|
|
|
|
local result = {}
|
|
if mods.ctrl then
|
|
table.insert(result, "CTRL-")
|
|
end
|
|
if mods.shift then
|
|
table.insert(result, "SHIFT-")
|
|
end
|
|
if mods.alt then
|
|
table.insert(result, "ALT-")
|
|
end
|
|
return table.concat(result, "")
|
|
end
|
|
|
|
function UnitFrame:PrepareWheelBinds(bindings)
|
|
-- 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
|
|
|
|
-- Build a table with all lines of the scroll wheel + modifier combinations
|
|
-- we want to bind, then concat it with newlines into a full script
|
|
local unit = self.unit
|
|
local bindScript = {
|
|
[[self:ClearBindings()]],
|
|
}
|
|
for _, bind in ipairs(bindings) do
|
|
if bind.button == "wheel-up" or bind.button == "wheel-down" then
|
|
local prefix = ModifiersToPrefix(bind.mods)
|
|
local button = bind.button == "wheel-up" and "MOUSEWHEELUP" or "MOUSEWHEELDOWN"
|
|
table.insert(bindScript,
|
|
string.format([[self:SetBindingClick(false, "%s%s", "OmicronSecureFrame%s", "%s%s")]],
|
|
prefix, button, unit, prefix, bind.button
|
|
)
|
|
)
|
|
end
|
|
end
|
|
|
|
local secure = self.secureFrame
|
|
local bindScript = table.concat(bindScript, "\n")
|
|
|
|
local removeBindScript = [[
|
|
self:ClearBindings()
|
|
]]
|
|
secure:SetAttribute("_onenter", bindScript)
|
|
secure:SetAttribute("_onleave", removeBindScript)
|
|
secure:SetAttribute("_onshow", removeBindScript)
|
|
secure:SetAttribute("_onhide", removeBindScript)
|
|
end
|
|
|
|
function UnitFrame:SetMouseBinds(binds)
|
|
local secure = self.secureFrame
|
|
local unit = self.unit
|
|
|
|
secure:RegisterForClicks("AnyDown")
|
|
self:PrepareWheelBinds(binds)
|
|
|
|
for _, bind in ipairs(binds) do
|
|
local prefix = ModifiersToPrefix(bind.mods)
|
|
local button
|
|
-- This is kind of a mess but there are two options for attribute names
|
|
-- in the normal click case (mouse1, mouse2, etc) we have to put the
|
|
-- modifiers before type so it would become SHIFT-type1 for mouse1 with
|
|
-- shift
|
|
--
|
|
-- Since scroll wheel is not supported here and we manually create
|
|
-- click events with global scroll wheel binds while we are moused over
|
|
-- the unit frame they receive the modifiers after the type so it would become
|
|
-- type-SHIFT-wheel-up for wheel-up with shift
|
|
if bind.button:find("mouse", 1, true) == 1 then
|
|
button = prefix .. bind.button:gsub("^mouse", "type")
|
|
elseif bind.button:find("wheel", 1, true) == 1 then
|
|
button = "type-" .. prefix .. bind.button
|
|
else
|
|
error("Keybinds were invalid")
|
|
end
|
|
if bind.kind == "macro" then
|
|
self:SetMacroAction(button, bind.data)
|
|
elseif bind.kind == "spell" then
|
|
self:SetSpellAction(button, bind.data)
|
|
else
|
|
secure:SetAttribute(button, bind.kind)
|
|
end
|
|
end
|
|
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)
|
|
|
|
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:ROLE_CHANGED_INFORM(name, changer, old, new)
|
|
if UnitName(self.unit) == name then
|
|
self:UpdateRole(new)
|
|
end
|
|
end
|
|
|
|
function UnitFrame:UNIT_POWER_UPDATE()
|
|
self:UpdatePower()
|
|
end
|
|
|
|
function UnitFrame:UpdateRole(role)
|
|
if role == nil then
|
|
role = UnitGroupRolesAssigned(self.unit)
|
|
end
|
|
if role == "HEALER" then
|
|
self:EnablePower(Enum.PowerType.Mana)
|
|
elseif role == "TANK" and select(3, UnitClass(self.unit)) == 6 then
|
|
self:EnablePower(Enum.PowerType.RunicPower)
|
|
else
|
|
self:DisablePower()
|
|
end
|
|
end
|
|
|
|
function UnitFrame:UpdatePower()
|
|
local power = UnitPower(self.unit, self.powerType)
|
|
local max = UnitPowerMax(self.unit, self.powerType)
|
|
self.power:SetRange(0, max)
|
|
self.power:SetValue(power)
|
|
end
|
|
|
|
function UnitFrame:EnablePower(type)
|
|
self.powerType = type
|
|
local color = PowerBarColor[type]
|
|
self.power:SetColor(color.r, color.g, color.b)
|
|
self.power:Show()
|
|
self.secureFrame:RegisterUnitEvent("UNIT_POWER_UPDATE", self.unit)
|
|
self:UpdatePower()
|
|
end
|
|
|
|
function UnitFrame:DisablePower()
|
|
self.powerType = nil
|
|
self.secureFrame:UnregisterEvent("UNIT_POWER_UPDATE")
|
|
self.power:Hide()
|
|
end
|
|
|
|
function UnitFrame:OnShow()
|
|
self.secureFrame:RegisterEvent("ROLE_CHANGED_INFORM")
|
|
self:StartRangeTicker()
|
|
self:UpdateAll(self:HasUnitChanged())
|
|
end
|
|
|
|
function UnitFrame:OnHide()
|
|
self.secureFrame:UnregisterEvent("ROLE_CHANGED_INFORM")
|
|
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()
|
|
self:UpdateRole() -- Also calls UpdatePower if power is visible
|
|
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)
|
|
-- TODO: UnitClass can return Unknown and the color can be nil. Having
|
|
-- the unit exist but not fully loaded is possibly a state we want to
|
|
-- handle more generally
|
|
local color = RAID_CLASS_COLORS[class] or {r=1, g=1, b=1}
|
|
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
|
|
]]--
|
|
|