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/ChorusRangeFrameTemplate.lua (2c3b10cd642cc62ddffe2275b243e5f456a4c1a1) (6247 bytes) (mode 100644) (type blob)

--[[--
@submodule chorus
]]

local GetSpellName = GetSpellName
local IsSpellInRange = IsSpellInRange

local MAX_SPELLS = MAX_SPELLS

local Chorus = Chorus

--[[--
Map of localized spell names to their maximum reach in yards.

@table spellRangeMap
@see spellRangeMapUpdate
@see rangeFrameUpdate
]]
local spellRangeMap = {}

Chorus.spellRangeMap = spellRangeMap

--[[--
Read player spell book and initialize `spellRangeMap` appropriately.

@see FrameXML/SpellBookFrame.lua:MAX_SPELLS
@see spellRangeMap
@function spellRangeMapUpdate
]]
local function spellRangeMapUpdate()
	local maxSpells = MAX_SPELLS or 1024
	maxSpells = math.min(math.max(1, math.abs(math.floor(maxSpells)), 8192))

	local buffer = {}

	local i = 0
	while (i < maxSpells) do
		i = i + 1

		--[[ Get player spell book button number "i", and request it's
		     corresponding spell name. This is NOT a unique spell
		     identifier. Every spell rank is a separate button. ]]--

		--[[-- @fixme Currently range indicator does not work in
		Cataclysm client (interface 40300). Specifically, `function
		GetSpellName` is not defined for that client.  Additionally,
		the current implementation may violate some additional
		restrictions found in interface `40300` but not in interface
		`30300`.]]

		--[[-- @fixme function GetSpellName is not available in Cata
		client.]]

		local spellName = GetSpellName(i, 'player')
		if not spellName then
			break
		end

		--[[ When GetSpellInfo is called with localized spell name
		     string, it only returns data on the spells stored in the
		     current player character's spell book. ]]--
		local _, _, _, _, _, _, _, _, maxRangeYards = GetSpellInfo(spellName)
		if spellName and maxRangeYards then
			assert(spellName ~= nil)
			assert('string' == type(spellName))
			spellName = strtrim(spellName)
			assert(string.len(spellName) >= 1)
			assert(string.len(spellName) <= 8192)

			assert(maxRangeYards ~= nil)
			assert('number' == type(maxRangeYards))
			maxRangeYards = math.abs(math.floor(maxRangeYards))

			if maxRangeYards >= 1 then
				local oldRangeYards = spellRangeMap[spellName]

				--[[
				Previous rank of the spell already mapped.
				Only the most potentially efficient spell rank
				will be mapped. ]]--

				if oldRangeYards then
					maxRangeYards = math.max(maxRangeYards, oldRangeYards)
				end

				--[[
				Priest spell "Mind Vision" has range of 50_000
				yards. This isn't a useful specificity, so
				filter it out. Only allow spell ranges that can
				be reasonably displayed and understood by the
				user during game combat play. Do not round down
				the value itself. Only store correct
				values.]]--
				if maxRangeYards < 99 then
					buffer[spellName] = maxRangeYards
				end
			end
		end
	end

	--[[ At this point assume no errors occurred. Re-write the actual spell
	     range map. ]]--
	spellRangeMap = buffer
end

--[[--
Update range frame to show approximate amount of yards to the given unit.

@see spellRangeMap

@function rangeFrameUpdate
@tparam frame self this range frame
]]
local function rangeFrameUpdate(self)
	assert(self ~= nil)

	local unitDesignation = SecureButton_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)

	local label = self.label or _G[self:GetName() .. 'Text']
	assert(label ~= nil)

	--[[ Frame must be always shown to keep update processor running. ]]--
	if not UnitExists(unitDesignation) or UnitIsUnit('player', unitDesignation) then
		label:SetText(nil)
		return
	end

	local rangeFlag = nil
	local distanceYards = nil
	local maxRangeYards = 0
	for spellName, rangeYards in pairs(spellRangeMap) do
		local flag = IsSpellInRange(spellName, unitDesignation)
		--[[ 1 == flag: in range; ]]--
		--[[ 0 == flag: out of range; ]]--
		--[[ nil == flag: not applicable or cannot be cast on given target. ]]--
		if not rangeFlag and flag then
			rangeFlag = flag
		end
		if nil ~= flag then
			maxRangeYards = math.max(rangeYards, maxRangeYards)
		end
		if 1 == flag then
			rangeFlag = 1
			if not distanceYards then
				distanceYards = rangeYards
			end
			distanceYards = math.min(distanceYards, rangeYards)
		end
	end

	local t
	if 1 == rangeFlag then
		t = string.format('<%d yd', distanceYards)
		label:SetTextColor(1, 1, 1)
	elseif 0 == rangeFlag then
		t = string.format('>%d yd', maxRangeYards)
		label:SetTextColor(1, 0, 0)
	else
		t = nil
	end
	label:SetText(t)
end

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

	--[[ Reduce update frequency to roughly 6 frames per second. ]]--
	if self.lastUpdateInstance and 'number' == type(self.lastUpdateInstance) then
		local now = GetTime()
		if now - self.lastUpdateInstance > 0.1667 then
			self.lastUpdateInstance = now
		else
			return
		end
	end

	rangeFrameUpdate(self)
end

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

	self.label = _G[self:GetName() .. 'Text']
	self:SetScript('OnUpdate', rangeFrameUpdateProcessor)
end

--[[--
`ChorusRangeFrameTemplate` displays approximate distance from player character
to the corresponding unit, in yards.

This works by checking the reach of each spell in the player's character's
current spell book. This ensures that the range approximation feature works in
any client, regardless of localization. This also allows the feature to span
several different WoW API interface versions more easily.

The spell range map is updated every time the player character learns a new
spell or enters the world.

@function rangeSpellMapFrameMain
@tparam frame self this spell map frame
]]--
local function rangeSpellMapFrameMain(self)
	assert(self ~= nil)

	self:RegisterEvent('ACTIVE_TALENT_GROUP_CHANGED')
	self:RegisterEvent('LEARNED_SPELL_IN_TAB')
	self:RegisterEvent('PLAYER_LOGIN')
	self:RegisterEvent('PLAYER_TALENT_UPDATE')
	self:RegisterEvent('SPELLS_CHANGED')
	self:SetScript('OnEvent', spellRangeMapUpdate)
end

--[[ Hide reference to the internal function, for some reason. ]]--
Chorus.rangeFrameMain = function(...)
	return rangeFrameMain(...)
end

Chorus.rangeSpellMapFrameMain = function(...)
	return rangeSpellMapFrameMain(...)
end


Mode Type Size Ref File
100644 blob 35 5c40e6e2862d70b5c51c326a13073a4012ac05c7 .gitignore
100644 blob 3606 f73da473168d1897963fd2e32d89841ca0461ec0 README.adoc
040000 tree - 271296b4bafcaa151458a0192fd313641ca9b409 bin
100644 blob 228 c7dd24afa7d5c2375ff60a91c73623a304b808f9 chorus.toc
040000 tree - 99c99c3cbc641f8954a5be291e61681cb5e74629 conf
040000 tree - efa7258757edf7b888ea13c215e14b497fef8a16 doc
100644 blob 2391 1b0ca1bc25f74a34476360e5a8f14e28767b204e makefile
040000 tree - b9d3ea9d61b99ee71b5fcdbc6b18843df20c8c3c 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