Files
florian.berthold 68701d7d42
release / release (push) Successful in 3s
feat(TankMode): CoA custom-class role detection via active spec
Add CoAClassSpecData.lua (copied from coa-db/data) and wire
GetPlayerCoARole() into TankMode so CoA tokens resolve TANK/HEALER/DAMAGER
from their active spec instead of defaulting to DAMAGER.

- CoAClassSpecData.lua: defines CoAClassSpec keyed by UnitClass token,
  loaded in .toc before Core.xml/Modules.xml
- GetPlayerCoARole(): pcall-guarded helper that calls
  SpecializationUtil.GetActiveSpecialization() (1-based index) to look up
  the current spec in CoAClassSpec[token].specs; TANK > HEALER > DAMAGER
  precedence when a spec carries multiple roles; returns nil for vanilla
  classes so the existing IsTank/IsHealer path is unchanged
- mod:Update(): try GetPlayerCoARole() first; fall through to vanilla
  logic only when nil (vanilla class or unresolvable spec)
- Re-evaluation on spec change already covered: PLAYER_TALENT_UPDATE
  fires on both spec and talent changes, driving mod:Update()

luac -p: TankMode.lua OK, CoAClassSpecData.lua OK
2026-05-30 01:28:59 +02:00

428 lines
12 KiB
Lua

--[[
-- Kui_Nameplates
-- By Kesava at curse.com
-- All rights reserved
-- Backported by: Kader at https://github.com/bkader
]]
local addon = LibStub("AceAddon-3.0"):GetAddon("KuiNameplates")
local mod = addon:NewModule("TankMode", addon.Prototype, "AceEvent-3.0")
local L = LibStub("AceLocale-3.0"):GetLocale("KuiNameplates")
-- `class` is resolved in mod:OnEnable() rather than at file scope: on the CoA
-- client this file can be parsed before PLAYER_LOGIN, when UnitClass("player")
-- returns nil and would otherwise leave `class` permanently nil.
local class, tankmode = nil, nil
local profile_tankmode
mod.uiName = L["Threat"]
-------------------------------------------------------- threat bracket stuff --
local function ShowThreatBrackets(frame, ...)
if not frame.threatBrackets then
return
end
if ... == false then
frame.threatBrackets:Hide()
else
frame.threatBrackets:SetVertexColor(...)
frame.threatBrackets:Show()
end
end
do
local brackets = {
{"BOTTOMLEFT", nil, "TOPLEFT"},
{"BOTTOMRIGHT", nil, "TOPRIGHT"},
{"TOPLEFT", nil, "BOTTOMLEFT"},
{"TOPRIGHT", nil, "BOTTOMRIGHT"}
}
-- pixel positions
local leftmost = 0.28125
local bottommost = 0
local default_size = 18
local ratio = 2
local size, x_offset, y_offset
function mod:UpdateThreatBracketScaling()
size = default_size * self.db.profile.brackets.scale
x_offset = (size * ratio) * leftmost
y_offset = floor((size * bottommost) - 2)
end
function mod:CreateThreatBrackets(frame)
local tb = CreateFrame("Frame", nil, frame.health)
tb:SetFrameLevel(1) -- same as castbar/healthbar
tb:Hide()
for k, v in ipairs(brackets) do
local b = tb:CreateTexture(nil, "ARTWORK", nil, -1)
b:SetTexture("Interface\\AddOns\\Kui_Nameplates\\Media\\threat-bracket")
tb[k] = b
end
tb.SetVertexColor = function(self, ...)
for k, b in ipairs(self) do
b:SetVertexColor(...)
end
end
frame.threatBrackets = tb
self:UpdateThreatBrackets(frame)
end
function mod:UpdateThreatBrackets(frame)
-- apply scaling + positions to threat brackets on given frame
if not frame.threatBrackets then
return
end
for k, v in ipairs(brackets) do
local b = frame.threatBrackets[k]
b:SetSize(size * ratio, size)
if k % 2 == 0 then
v[4] = x_offset - 1
else
v[4] = -x_offset
end
if k <= 2 then
v[5] = -y_offset
else
v[5] = y_offset - .5
end
if k == 2 then
b:SetTexCoord(1, 0, 0, 1)
elseif k == 3 then
b:SetTexCoord(0, 1, 1, 0)
elseif k == 4 then
b:SetTexCoord(1, 0, 1, 0)
end
v[2] = frame.health
b:SetPoint(unpack(v))
end
end
end
--------------------------------------------------------- CoA role detection --
-- Returns "TANK", "HEALER", or "DAMAGER" for the player's current spec when
-- the player is a CoA custom class. Returns nil if the class is not in
-- CoAClassSpec (vanilla path) or if the active spec cannot be resolved (caller
-- falls back to existing logic).
--
-- API rationale: SpecializationUtil.GetActiveSpecialization() is the
-- confirmed per-spec API on the CoA/Ascension 3.3.5a client. It returns
-- a 1-based integer matching the in-game tab order, which is the same order
-- as CoAClassSpec[token].specs. This is the same API used by coa-clique
-- (Clique.lua:700) and coa-weakauras (WeakAuras.lua:1608) on this client.
-- We guard with pcall so a missing or broken API degrades silently to nil.
local function GetPlayerCoARole()
if not CoAClassSpec then return nil end
local token = select(2, UnitClass("player"))
if not token then return nil end
local classData = CoAClassSpec[token]
if not classData then return nil end -- vanilla class; use vanilla path
-- Resolve the active spec index via SpecializationUtil.
local specIndex
local ok, result = pcall(function()
if SpecializationUtil and SpecializationUtil.GetActiveSpecialization then
return SpecializationUtil.GetActiveSpecialization()
end
return nil
end)
if ok and result and result >= 1 then
specIndex = result
end
local specData
if specIndex and classData.specs[specIndex] then
specData = classData.specs[specIndex]
end
if not specData then return nil end
-- Map CoAClassSpec roles → tank-mode role.
-- TANK and HEALER take precedence over DAMAGER roles when both present.
local roles = specData.roles
if not roles then return nil end
local isTank, isHealer = false, false
for _, r in ipairs(roles) do
if r == "TANK" then isTank = true end
if r == "HEALER" then isHealer = true end
end
if isTank then return "TANK" end
if isHealer then return "HEALER" end
return "DAMAGER"
end
--------------------------------------------------------- tank mode functions --
do
local function getTalentpointsSpent(spellID)
local spellName = GetSpellInfo(spellID)
for tabIndex = 1, GetNumTalentTabs() do
for talentID = 1, GetNumTalents(tabIndex) do
local name, _, _, _, spent = GetTalentInfo(tabIndex, talentID)
if (name == spellName) then
return spent
end
end
end
return 0
end
local function IsDeathKnightTank()
-- idea taken from addon 'ElitistJerks'
local tankTalents =
(getTalentpointsSpent(16271) >= 5 and 1 or 0) + -- Anticipation
(getTalentpointsSpent(49042) >= 5 and 1 or 0) + -- Toughness
(getTalentpointsSpent(55225) >= 5 and 1 or 0) -- Blade Barrier
return tankTalents >= 2
end
local function IsDruidTank()
-- idea taken from addon 'ElitistJerks'
local tankTalents =
(getTalentpointsSpent(57881) >= 2 and 1 or 0) + -- Natural Reaction
(getTalentpointsSpent(16929) >= 3 and 1 or 0) + -- Thick Hide
(getTalentpointsSpent(61336) >= 1 and 1 or 0) + -- Survival Instincts
(getTalentpointsSpent(57877) >= 3 and 1 or 0) -- Protector of the Pack
return tankTalents >= 3
end
-- NOTE: only vanilla 3.3.5 classes are matched below. CoA custom classes
-- will fall through to DAMAGER in mod:Update(). If a CoA-specific tank /
-- healer class table becomes available we should consult it here instead
-- of (or in addition to) the hard-coded class strings.
local function IsTank()
return (class == "WARRIOR" and select(3, GetTalentTabInfo(3)) >= 51) or
(class == "DEATHKNIGHT" and IsDeathKnightTank()) or
(class == "PALADIN" and select(3, GetTalentTabInfo(2)) >= 51) or
(class == "DRUID" and select(3, GetTalentTabInfo(2)) >= 51 and IsDruidTank())
end
-- See IsTank() note: CoA custom healer classes fall through to DAMAGER.
local function IsHealer()
return (class == "PALADIN" and select(3, GetTalentTabInfo(1)) >= 51) or
(class == "SHAMAN" and select(3, GetTalentTabInfo(3)) >= 51) or
(class == "DRUID" and select(3, GetTalentTabInfo(3)) >= 51) or
(class == "PRIEST" and select(3, GetTalentTabInfo(3)) < 51)
end
function mod:Update()
if profile_tankmode.enabled == 1 then
-- smart - judge by spec
local role
-- For CoA custom classes, derive role from the active spec via
-- CoAClassSpec. GetPlayerCoARole() returns nil when the class is
-- vanilla (not in CoAClassSpec) or when the spec cannot be resolved,
-- so vanilla classes always fall through to the IsTank/IsHealer path.
local coaRole = GetPlayerCoARole()
if coaRole then
role = coaRole
elseif class == "WARRIOR" and GetShapeshiftForm() ~= 2 then
-- no tank for gladiator stance
role = nil
elseif IsTank() then
role = "TANK"
elseif IsHealer() then
role = "HEALER"
else
role = "DAMAGER"
end
if role == "TANK" then
tankmode = true
else
tankmode = false
end
else
tankmode = (profile_tankmode.enabled == 3)
end
end
end
function mod:Toggle()
if profile_tankmode.enabled == 1 then
-- smart tank mode, listen for spec changes
self:RegisterEvent("PLAYER_TALENT_UPDATE", "Update")
-- on a warrior, watch for gladiator stance
if class == "WARRIOR" then
self:RegisterEvent("UPDATE_SHAPESHIFT_FORM", "Update")
end
else
self:UnregisterEvent("PLAYER_TALENT_UPDATE")
self:UnregisterEvent("UPDATE_SHAPESHIFT_FORM")
end
self:Update()
end
function mod:ThreatUpdate(frame)
frame.hasThreat = true
-- we are holding threat if the default glow is red
frame.holdingThreat = frame.glow.r > .9 and (frame.glow.g + frame.glow.b) < .1
if not frame.targetGlow or not frame.target then
if tankmode then
-- set glow to tank colour unless this is the current target
frame:SetGlowColour(unpack(profile_tankmode.glowcolour))
else
-- not in tank mode; set glow to default ui's colour
frame:SetGlowColour(frame.glow.r, frame.glow.g, frame.glow.b)
end
end
if tankmode then
-- also change health bar colour in tank mode
if frame.holdingThreat then
frame:SetHealthColour(10, unpack(profile_tankmode.barcolour))
ShowThreatBrackets(frame, unpack(profile_tankmode.barcolour))
else
-- losing/gaining threat
frame:SetHealthColour(10, unpack(profile_tankmode.midcolour))
ShowThreatBrackets(frame, unpack(profile_tankmode.midcolour))
end
else
-- not in tank mode; use default glow colour for brackets, too
ShowThreatBrackets(frame, frame.glow.r, frame.glow.g, frame.glow.b)
end
end
function mod:ThreatClear(frame)
frame:SetHealthColour(false)
ShowThreatBrackets(frame, false)
end
-------------------------------------------------------------------- messages --
function mod:PostCreate(msg, f)
self:CreateThreatBrackets(f)
end
function mod:PostHide(msg, f)
ShowThreatBrackets(f, false)
end
---------------------------------------------------- Post db change functions --
mod:AddConfigChanged("enabled", function() mod:Toggle() end)
mod:AddConfigChanged(
{"brackets", "scale"},
function()
mod:UpdateThreatBracketScaling()
end,
function(f)
mod:UpdateThreatBrackets(f)
end
)
-------------------------------------------------------------------- Register --
function mod:GetOptions()
return {
tankmode = {
type = "group",
name = L["Tank mode"],
inline = true,
order = 10,
disabled = function(info)
return mod.db.profile.tankmode.enabled == 2
end,
args = {
enabled = {
type = "select",
name = L["Enable tank mode"],
desc = L['Change the colour of a plate\'s health bar and border when you have threat on its unit.\n\nSelecting "Smart" (default) will automatically enable or disable tank mode based on your current specialisation\'s role.'],
values = {"Smart", "Disabled", "Enabled"},
order = 0,
width = "double",
disabled = false
},
barcolour = {
type = "color",
name = L["Bar colour"],
desc = L["The bar colour to use when you have threat"],
order = 10,
width = "half"
},
midcolour = {
type = "color",
name = L["Transitional colour"],
desc = L["The bar colour to use when you are losing or gaining threat."],
order = 20,
width = "half"
},
glowcolour = {
type = "color",
name = L["Glow colour"],
desc = L["The glow (border) colour to use when you have threat"],
hasAlpha = true,
order = 30,
width = "half"
}
}
},
brackets = {
type = "group",
name = L["Threat brackets"],
inline = true,
order = 20,
disabled = function(info)
return not mod.db.profile.brackets.enable_brackets
end,
args = {
enable_brackets = {
type = "toggle",
name = L["Show threat brackets"],
desc = L["Show threat brackets when you have threat on a nameplate. Kind of like target arrows, but for threat. In tank mode they will inherit the bar colour set above. Otherwise they will use the default glow colour."],
order = 10,
disabled = false
},
scale = {
type = "range",
name = L["Threat bracket scale"],
desc = L["The scale of the threat bracket textures"],
order = 20,
min = 0.1,
softMin = 0.5,
softMax = 2
}
}
}
}
end
function mod:configChangedListener()
profile_tankmode = self.db.profile.tankmode
end
function mod:OnInitialize()
self.db = addon.db:RegisterNamespace(self.moduleName, {profile = {
tankmode = {
enabled = 1,
barcolour = {.2, .9, .1},
midcolour = {1, .5, 0},
glowcolour = {1, 0, 0, 1}
},
brackets = {
enable_brackets = true,
scale = 1
}
}})
addon:InitModuleOptions(self)
self:UpdateThreatBracketScaling()
self:SetEnabledState(true)
end
function mod:OnEnable()
class = select(2, UnitClass("player"))
if self.db.profile.brackets.enable_brackets then
self:RegisterMessage("KuiNameplates_PostCreate", "PostCreate")
self:RegisterMessage("KuiNameplates_PostHide", "PostHide")
end
self:Toggle()
end