vrtc / chorus (public) (License: CC0) (since 2023-08-12) (hash sha1)
World of Warcraft add-on stub. The overall goal is to create a specialized raid frame.

/src/ChorusProgressFrameTemplate.lua (ece5e637179444fc616526fba28e7cf2e080ec80) (12578 bytes) (mode 100644) (type blob)

local PowerBarColor = PowerBarColor
local RAID_CLASS_COLORS = RAID_CLASS_COLORS
local UnitClass = UnitClass
local UnitExists = UnitExists
local UnitHealth = UnitHealth
local UnitHealthMax = UnitHealthMax
local UnitIsConnected = UnitIsConnected
local UnitIsCorpse = UnitIsCorpse
local UnitIsDead = UnitIsDead
local UnitIsGhost = UnitIsGhost
local UnitPower = UnitPower
local UnitPowerMax = UnitPowerMax
local UnitPowerType = UnitPowerType

local Chorus = Chorus

local function getUnit(f)
	local p = f:GetParent()
	local u = f.unit or f:GetAttribute('unit')
	if not u and p then
		u = p.unit or p:GetAttribute('unit')
	end
	return u
end

local function validateProgressFrame(self)
	assert(self ~= nil)

	local artwork = self.artwork
	assert(artwork ~= nil)

	local label1 = self.label1
	assert(label1 ~= nil)

	local label2 = self.label2
	assert(label2 ~= nil)

	local label3 = self.label3
	assert(label3 ~= nil)

	local ratio = self:GetValue()
	assert(ratio ~= nil)
	assert('number' == type(ratio))
	assert(ratio >= 0.0)
	assert(ratio <= 1.0)

	local strategy = self.strategy
	if strategy then
		assert('string' == type(strategy))
		strategy = strtrim(strategy)
		assert(string.len(strategy) >= 1)
		assert(string.len(strategy) <= 8192)

		local unitDesignation = getUnit(self) or 'none'
		assert('string' == type(unitDesignation))
		unitDesignation = string.lower(strtrim(unitDesignation))
		assert(string.len(unitDesignation) >= 1)
		assert(string.len(unitDesignation) <= 256)
	end
end

local function getRatio(a, b)
	assert(a ~= nil)
	assert('number' == type(a))
	assert(a >= 0)

	assert(b ~= nil)
	assert('number' == type(b))
	assert(b > 0)

	assert(a <= b)

	local x = math.min(math.abs(a), math.abs(b))
	local y = math.max(math.abs(a), math.abs(b))
	y = math.max(y, 1)
	assert(y > 0)

	local ratio = x / y
	ratio = math.min(math.max(0, math.abs(ratio)), 1)

	return ratio
end

local function applySize(self)
	assert(self ~= nil)

	local label1 = self.label1
	assert(label1 ~= nil)

	local label2 = self.label2
	assert(label2 ~= nil)

	local label3 = self.label3
	assert(label3 ~= nil)

	if self:GetHeight() >= 36 then
		label1:Show()
		label2:Show()
		label3:Show()
	elseif self:GetHeight() >= 12 then
		label1:Show()
		label2:Show()
		label3:Hide()
	else
		label1:Hide()
		label2:Hide()
		label3:Hide()
	end
end

local function applyRatio(self, a, b)
	assert(self ~= nil)

	local ratio = 0

	--[[ Strict sanitization to work around WoW API quirks. ]]--
	if a and b then
		assert('number' == type(a))
		assert('number' == type(b))
		local x = math.min(a, b)
		local y = math.max(a, b)
		if x <= y then
			ratio = getRatio(x, y)
		end
	end

	self:SetValue(ratio)
end

local function formatQuantity(quantity)
	assert(quantity ~= nil)
	assert('number' == type(quantity))

	local t
	if math.abs(quantity) < 1000 then
		t = string.format('%d',quantity)
	elseif math.abs(quantity) < 1000000 then
		t = string.format('%.2f K', quantity / 1000)
	else
		t = string.format('%.2f M', quantity /  1000000)
	end

	return t
end

local function applyOverlay(self, a, b)
	assert(self ~= nil)

	assert(a ~= nil)
	assert('number' == type(a))
	assert(a >= 0)

	assert(b ~= nil)
	assert('number' == type(b))
	--[[ Work around quirks of the native API. ]]--
	b = math.max(b, 1)
	assert(b > 0)

	assert(a <= b)

	local ratio = getRatio(a, b)

	local label1 = self.label1 or _G[self:GetName() .. 'Text1']
	assert(label1 ~= nil)
	local t = formatQuantity(a)
	label1:SetText(t)

	local label2 = self.label2 or _G[self:GetName() .. 'Text2']
	assert(label2 ~= nil)
	local e = string.format('%.0f%%', ratio * 100)
	label2:SetText(e)

	--[[ ShadowedUnitFrames recommend this approach for adjusting font size. ]]--
	if t ~= nil and string.len(t) >= 1 then
		local fontSize = 12
		self:SetScale(label1:GetStringHeight() / fontSize)
	end
end

local function applyRatioHealth(self, unitDesignation)
	assert(self ~= nil)

	assert(unitDesignation ~= nil)

	local a
	local b
	if not UnitExists(unitDesignation) or
	   not UnitIsConnected(unitDesignation) or
	   UnitIsDead(unitDesignation) or
	   UnitIsCorpse(unitDesignation) or
	   UnitIsGhost(unitDesignation) then
		a = 0
		b = 1
	else
		a = UnitHealth(unitDesignation) or 0
		b = UnitHealthMax(unitDesignation) or 1
	end

	applyRatio(self, a, b)
end

local function applyOverlayHealthDeficit(self, a, b)
	assert(self ~= nil)

	local label3 = self.label3 or _G[self:GetName() .. 'Text3']
	assert(label3 ~= nil)
	if a and b and a >= 1 and b >= 1 and a < b then
		local c = a - b
		local t = formatQuantity(c)
		label3:SetText(t)
	else
		label3:SetText(nil)
	end
end

local function applyOverlayHealth(self, unitDesignation)
	assert(self ~= nil)

	assert(unitDesignation ~= nil)

	local label1 = self.label1
	assert(label1 ~= nil)

	local label2 = self.label2
	assert(label2 ~= nil)

	local label3 = self.label3
	assert(label3 ~= nil)

	local u = unitDesignation
	local a = UnitHealth(u)
	local b = UnitHealthMax(u)

	if a and b and a <= b and b >= 1 and not UnitIsDead(u) and not UnitIsCorpse(u) and not UnitIsGhost(u) then
		applyOverlay(self, a, b)
		applyOverlayHealthDeficit(self, a, b)
	else
		label1:SetText(nil)
		label2:SetText(nil)
		label3:SetText(nil)
		--[[ TODO Separate health status bar and unit statues. ]]--
		if UnitIsCorpse(unitDesignation) then
			label1:SetText('(Corpse)')
		elseif UnitIsGhost(unitDesignation) then
			label1:SetText('(Ghost)')
		elseif UnitIsDead(unitDesignation) then
			label1:SetText('(Dead)')
		end
	end
end

local function applyRatioPower(self, unitDesignation, powerTypeEnum)
	assert(self ~= nil)
	assert(unitDesignation ~= nil)
	assert(powerTypeEnum ~= nil)

	local a = UnitPower(unitDesignation, powerTypeEnum) or 0
	local b = UnitPowerMax(unitDesignation, powerTypeEnum) or 1
	applyRatio(self, a, b)
end

local function applyOverlayPower(self, unitDesignation, powerTypeEnum)
	assert(self ~= nil)
	assert(unitDesignation ~= nil)
	assert(powerTypeEnum ~= nil)

	local a = UnitPower(unitDesignation, powerTypeEnum) or 0
	local b = UnitPowerMax(unitDesignation, powerTypeEnum) or 1
	applyOverlay(self, a, b)
end

local function applyHealthFrameColorUnitClass(self, unitDesignation)
	assert(self ~= nil)
	assert(unitDesignation ~= nil)

	local _, classDesignation = UnitClass(unitDesignation)
	if not classDesignation then
		return
	end

	local map = RAID_CLASS_COLORS
	assert(map ~= nil)
	assert('table' == type(map))
	local colorTuple = map[classDesignation]

	local r = 1
	local g = 1
	local b = 1
	if colorTuple then
		assert('table' == type(colorTuple))
		r = math.min(math.max(0, math.abs(colorTuple.r)), 1)
		g = math.min(math.max(0, math.abs(colorTuple.g)), 1)
		b = math.min(math.max(0, math.abs(colorTuple.b)), 1)
	end

	local artwork = self.artwork
	assert(artwork ~= nil)

	local image = artwork:GetTexture()
	if  image then
		artwork:SetVertexColor(r, g, b)
	else
		artwork:SetTexture(r, g, b)
	end
end

local function applyHealthFrameColorUnitIsFriend(self, unitDesignation)
	assert(self ~= nil)
	assert(unitDesignation ~= nil)

	local r = 255 / 255
	local g = 215 / 255
	local b = 0 / 255

	if UnitIsFriend('player', unitDesignation) then
		r = 124 / 255
		g = 252 / 255
		b = 0 / 255
	elseif UnitIsEnemy('player', unitDesignation) then
		r = 255 / 255
		g = 69 / 255
		b = 0 / 255
	end

	local artwork = self.artwork
	assert(artwork ~= nil)

	local image = artwork:GetTexture()
	if  image then
		artwork:SetVertexColor(r, g, b)
	else
		artwork:SetTexture(r, g, b)
	end
end

local function healthFrameEventProcessor(self)
	validateProgressFrame(self)

	local unitDesignation = getUnit(self) or 'none'

	assert(unitDesignation ~= nil)
	assert('string' == type(unitDesignation))
	unitDesignation = string.lower(strtrim(unitDesignation))
	assert(string.len(unitDesignation) >= 1)
	assert(string.len(unitDesignation) <= 256)

	if UnitExists(unitDesignation) and UnitIsConnected(unitDesignation) then
		self:Show()
	else
		self:Hide()
		return
	end

	applySize(self)
	applyRatioHealth(self, unitDesignation)
	applyOverlayHealth(self, unitDesignation)

	local strategy = self.strategy
	assert(strategy ~= nil)
	assert('string' == type(strategy))
	strategy = strtrim(strategy)
	assert(string.len(strategy) >= 1)
	assert(string.len(strategy) <= 256)

	assert(strategy == 'UnitClass' or strategy == 'UnitIsFriend')

	if 'UnitClass' == strategy then
		applyHealthFrameColorUnitClass(self, unitDesignation)
	elseif 'UnitIsFriend' == strategy then
		applyHealthFrameColorUnitIsFriend(self, unitDesignation)
	else
		error('invalid enum: strategy must be either UnitClass or UnitIsFriend')
	end
end

local function applyPowerFrameColor(self, powerTypeEnum)
	assert(self ~= nil)

	local r = 1
	local g = 0
	local b = 1
	if powerTypeEnum then
		--[[ See FrameXML/UnitFrame.lua:PowerBarColor ]]--
		local map = PowerBarColor
		assert(map ~= nil)
		assert('table' == type(map))

		local colorTuple = map[powerTypeEnum]
		if colorTuple then
			assert('table' == type(colorTuple))
			r = math.min(math.max(0, math.abs(colorTuple.r)), 1)
			g = math.min(math.max(0, math.abs(colorTuple.g)), 1)
			b = math.min(math.max(0, math.abs(colorTuple.b)), 1)
		end
	end

	local artwork = self.artwork
	assert(artwork ~= nil)

	local image = artwork:GetTexture()
	if  image then
		artwork:SetVertexColor(r, g, b)
	else
		artwork:SetTexture(r, g, b)
	end
end

local function powerFrameEventProcessor(self)
	validateProgressFrame(self)

	local unitDesignation = getUnit(self) or 'none'

	assert(unitDesignation ~= nil)
	assert('string' == type(unitDesignation))
	unitDesignation = string.lower(strtrim(unitDesignation))
	assert(string.len(unitDesignation) >= 1)
	assert(string.len(unitDesignation) <= 256)

	if UnitExists(unitDesignation) and UnitIsConnected(unitDesignation) then
		self:Show()
	else
		self:Hide()
		return
	end

	local powerTypeEnum
	local strategy = self.strategy
	assert(strategy ~= nil)
	assert('string' == type(strategy))
	strategy = strtrim(strategy)
	assert(string.len(strategy) >= 1)
	assert(string.len(strategy) <= 256)
	if 'UnitPowerType' == strategy then
		local enum, category = UnitPowerType(unitDesignation)
		if category then
			powerTypeEnum = enum
		else
			self:Hide()
			return
		end
	else
		--[[ See FrameXML/Constants.lua:SPELL_POWER_MANA ]]--
		powerTypeEnum = _G[strategy]
	end
	assert(powerTypeEnum ~= nil)
	assert('number' == type(powerTypeEnum))
	assert(powerTypeEnum >= 0)
	powerTypeEnum = math.min(math.max(0, math.floor(math.abs(powerTypeEnum))), 8192)

	if UnitPowerMax(unitDesignation, powerTypeEnum) > 0 then
		self:Show()
		applySize(self)
		applyRatioPower(self, unitDesignation, powerTypeEnum)
		applyOverlayPower(self, unitDesignation, powerTypeEnum)
		applyPowerFrameColor(self, powerTypeEnum)
	else
		self:Hide()
	end
end

--[[ TODO Maybe make progress frame completely generic, by removing the unit field. ]]--
function Chorus.progressFrameMain(self)
	assert(self ~= nil)

	local frameDesignation = self:GetName()
	assert(frameDesignation ~= nil)
	assert('string' == type(frameDesignation))
	frameDesignation = strtrim(frameDesignation)
	assert(string.len(frameDesignation) >= 1)
	assert(string.len(frameDesignation) <= 256)

	local artwork = _G[frameDesignation .. 'Artwork']
	assert(artwork ~= nil)
	self.artwork = artwork

	local label1 = _G[frameDesignation .. 'Text1']
	assert(label1 ~= nil)
	self.label1 = label1

	local label2 = _G[frameDesignation .. 'Text2']
	assert(label2 ~= nil)
	self.label2 = label2

	local label3 = _G[frameDesignation .. 'Text3']
	assert(label3 ~= nil)
	self.label3 = label3

	self:RegisterEvent('PARTY_CONVERTED_TO_RAID')
	self:RegisterEvent('PARTY_MEMBERS_CHANGED')
	self:RegisterEvent('PARTY_MEMBER_DISABLE')
	self:RegisterEvent('PARTY_MEMBER_ENABLE')
	self:RegisterEvent('PLAYER_ALIVE')
	self:RegisterEvent('PLAYER_FOCUS_CHANGED')
	self:RegisterEvent('PLAYER_LOGIN')
	self:RegisterEvent('PLAYER_TARGET_CHANGED')
	self:RegisterEvent('RAID_ROSTER_UPDATE')

	self:SetMinMaxValues(0, 1)
	self:SetValue(1)
end

function Chorus.healthFrameMain(self, ...)
	Chorus.progressFrameMain(self, ...)
	self.strategy = 'UnitIsFriend'
	self:SetScript('OnEvent', healthFrameEventProcessor)

	self:RegisterEvent('UNIT_HEALTH')
	--[[ TODO Add health deficit label ]]--
end

function Chorus.powerFrameMain(self, ...)
	Chorus.progressFrameMain(self, ...)
	self.strategy = 'UnitPowerType'
	self:SetScript('OnEvent', powerFrameEventProcessor)

	self:RegisterEvent('UNIT_ENERGY')
	self:RegisterEvent('UNIT_MANA')
	self:RegisterEvent('UNIT_MAXPOWER')
	self:RegisterEvent('UNIT_POWER')
	self:RegisterEvent('UNIT_RAGE')
	self:RegisterEvent('UNIT_RUNIC_POWER')
end


Mode Type Size Ref File
100644 blob 2761 8660a7b6b158b3d7c82af258020ea93722ae2bee README.md
040000 tree - 386cdd1448b396d67052c9c421df4d307621c280 bin
100644 blob 255 4ea44ab0938378fbfd697b6a70e318371d15570c chorus-0.1-1.rockspec
100644 blob 111 b22afedfa744551589b080882fdc70c11ab6b17d chorus.toc
040000 tree - de9110b7af6abd67b529e6b6a844de41c3d37373 etc
040000 tree - 5b8dd064c04749cf19281ed90842d3996ad61dc6 share
040000 tree - ee0d05f4e288db1bae1bc0d1b6582e34caea5c0c src
Hints:
Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://rocketgit.com/user/vrtc/chorus

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@ssh.rocketgit.com/user/vrtc/chorus

Clone this repository using git:
git clone git://git.rocketgit.com/user/vrtc/chorus

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a merge request:
... clone the repository ...
... make some changes and some commits ...
git push origin main