Files
github-actions[bot] 9588a9cf0a WeakestAuras 0.18.0
ClassicAPI 1.18 is now the only requirement

Requirements
- Needs ClassicAPI 1.18.0 or newer. SuperWoW is no longer used, and Nampower is optional.
- On an older ClassicAPI the addon stays off, and a popup links the download.
- Custom triggers on SuperWoW or Nampower events only fire while that mod is installed.

Triggers
- New Combat Log trigger, with lists of spell IDs, spell names and NPC IDs.
  WeakAuras2 auras that use one now import.
- Custom triggers can listen to the combat log with CLEU or CLEU:SUBEVENT.
- Crowd Control matches a specific type (stun, fear, root, silence, disarm and
  more) or a school lockout, with the effect's real timer.
- Saved Crowd Control auras: Root, Silence and Disarm filters carry over; CC and
  Snare become "any type", and snares are no longer detected.
- Cast bars follow pushback and channel updates.

Displays
- Icon cooldown swipes are drawn by the client's own cooldown frame.
- Sound actions play on a chosen channel, and a new Stop sound type ends a
  looping sound.

Fixes
- Cooldown auras no longer sweep on every wand shot.
- The Global Cooldown trigger ignores wand and Auto Shot shots.
2026-10-29 20:41:53 +00:00

397 lines
17 KiB
Lua

-- WeakestAuras -- the "subtext" sub-region: a placeholder-driven FontString a
-- display can carry any number of (stacks, remaining time, name...). Mirrors
-- WA2's SubText (§8/§9).
-- Upstream section refs (§N) point at design/architecture/weakauras2-reference.md
--
-- An instance is a plain object wrapping a FontString parented to the region.
-- It never polls: it subscribes to the region's subRegionEvents bus -- "Update"
-- (state changed) always, "FrameTick" only when its text contains %p, "PreShow"
-- to reassert visibility a condition may have cleared. Text is resolved through
-- WA.ReplacePlaceHolders against the region's active state, via the per-symbol
-- formatters built from this instance's text_text_format_* settings.
if WeakestAuras.disabled then return end
local WA = WeakestAuras
local proto = WA.regionPrototype
-- The nine standard frame anchor points offered by the Anchor dropdown.
local TEXT_ANCHORS = {
"TOPLEFT", "TOP", "TOPRIGHT",
"LEFT", "CENTER", "RIGHT",
"BOTTOMLEFT", "BOTTOM", "BOTTOMRIGHT",
}
-- Per-symbol format settings (WA.format_types) are flat keys on the sub-region
-- under upstream's layout, text_text_format_<symbol>_<setting>. Both the
-- formatter build and the options generator address them through these.
local function formatGetter(subData)
return function(key, default)
local v = subData["text_text_format_" .. key]
if v == nil then return default end
return v
end
end
-- Every string this text can end up showing: its own, plus any a condition can
-- swap in through text_text. Formatters are built once over the whole set, the
-- way the standalone text region does it (`displayTexts`), so a condition
-- supplying a %p hands over an already-formatted symbol rather than rendering
-- raw until the next modify. The scan is not narrowed to this instance's own
-- index -- modify is not told which index it is -- so a sibling's strings are
-- swept in too; a formatter for a symbol this text never shows is one unused
-- table entry.
local function subTexts(parentData, subData)
local texts = { subData.text_text or "" }
local conditions = parentData.conditions or {}
for i = 1, #conditions do
local changes = conditions[i].changes or {}
for c = 1, #changes do
local change = changes[c]
local _, _, suffix = string.find(change.property or "", "^sub%.[0-9]+%.(.*)$")
if suffix == "text_text" and type(change.value) == "string" then
table.insert(texts, change.value)
end
end
end
return texts
end
-- Upstream's own field values. LEFT reads bottom-to-top, RIGHT top-to-bottom.
WA.text_rotate_types = { NONE = "None", LEFT = "Left", RIGHT = "Right" }
WA.text_rotate_values = { "NONE", "LEFT", "RIGHT" }
local ROTATE_RADIANS = { LEFT = math.pi / 2, RIGHT = -math.pi / 2 }
-- Where a rotated string has to be nudged so the corner the user anchored still
-- lands where they put it.
--
-- SetRotation is VISUAL ONLY: GetStringWidth, GetStringHeight and the region's
-- rect all stay axis-aligned, so the engine goes on anchoring an unrotated w*h
-- box while the ink occupies an h*w box about the same centre. The centre is
-- the one point both agree on, which is why only off-centre anchors need
-- anything -- and the correction is just half the difference between the two
-- box dimensions, signed by which edge was anchored.
--
-- This is NOT upstream's getRotateOffset. Upstream rotates about the anchor
-- point (an AnimationGroup with SetOrigin, which this client has no equivalent
-- of); ClassicAPI rotates about the centre, so upstream's formula would correct
-- for a displacement that never happens here.
local function rotateOffset(fs, rotate, point)
if not rotate or rotate == "NONE" or not WA.hasInlineText then return 0, 0 end
local w = fs:GetStringWidth() or 0
local h = fs:GetStringHeight() or 0
local half = (h - w) / 2
local xo, yo = 0, 0
if string.find(point, "LEFT", 1, true) then xo = half
elseif string.find(point, "RIGHT", 1, true) then xo = -half end
if string.find(point, "TOP", 1, true) then yo = half
elseif string.find(point, "BOTTOM", 1, true) then yo = -half end
return xo, yo
end
WA.RegisterSubRegionType("subtext", {
displayName = "Text",
supports = function(regionType)
return regionType == "icon" or regionType == "progressbar" or regionType == "texture"
or regionType == "progresstexture" or regionType == "empty"
end,
default = {
type = "subtext",
text_text = "%s",
text_color = { 1, 1, 1, 1 },
text_fontSize = 12,
anchor_point = "CENTER",
anchorXOffset = 0,
anchorYOffset = 0,
text_visible = true,
rotateText = "NONE",
},
-- Icon: a %s stacks text bottom-right. Bar: %n name at the left, %p time at
-- the right (upstream's own new-aura defaults, §8).
addDefaultsForNewAura = function(data)
if data.regionType == "icon" then
table.insert(data.subRegions, {
type = "subtext", text_text = "%s", text_color = { 1, 1, 1, 1 },
text_fontSize = 12, anchor_point = "BOTTOMRIGHT", anchorXOffset = -2, anchorYOffset = 2,
text_visible = true,
})
elseif data.regionType == "progressbar" then
-- Offsets are from the *bar*, not the whole region (see modify below),
-- so neither has to leave room for an icon that may not be there.
table.insert(data.subRegions, {
type = "subtext", text_text = "%n", text_color = { 1, 1, 1, 1 },
text_fontSize = 10, anchor_point = "LEFT", anchorXOffset = 3, anchorYOffset = 0,
text_visible = true,
})
table.insert(data.subRegions, {
type = "subtext", text_text = "%p", text_color = { 1, 1, 1, 1 },
text_fontSize = 10, anchor_point = "RIGHT", anchorXOffset = -3, anchorYOffset = 0,
text_visible = true,
})
end
end,
-- Condition-overridable properties, namespaced sub.<n>.* (§8). Each setter
-- is a method defined in create below.
-- The offset keys keep upstream's `text_anchorXOffset` naming while pointing
-- `dataKey` at our own saved field (`anchorXOffset`, renamed from `text_x` at
-- internalVersion 3): WA2Import carries condition property names through
-- verbatim, so an imported condition only lands if the property is spelled
-- the way upstream spells it.
properties = {
text_visible = { display = "Visible", setter = "SetVisible", type = "bool" },
text_text = { display = "Text", setter = "ChangeText", type = "string" },
text_color = { display = "Color", setter = "SetTextColor", type = "color" },
text_fontSize = { display = "Font Size", setter = "SetTextHeight", type = "number", min = 6, softMax = 72, step = 1 },
text_anchorXOffset = { display = "X-Offset", setter = "SetXOffset", type = "number",
min = -500, max = 500, step = 1, dataKey = "anchorXOffset" },
text_anchorYOffset = { display = "Y-Offset", setter = "SetYOffset", type = "number",
min = -500, max = 500, step = 1, dataKey = "anchorYOffset" },
rotateText = { display = "Rotate Text", setter = "SetRotateText", type = "list",
values = "text_rotate_types" },
},
create = function(parent)
local region = { parent = parent }
-- The FontString lives in a frame of its own, not on the region: a child
-- frame's draw layers all sit above its parent's, so text created directly
-- on the region renders *under* anything the region type builds as a child
-- frame -- the progress bar, the icon's cooldown swipe. modifyFinish
-- (re)asserts the level after every modify, since SetParent resets it.
local frame = CreateFrame("Frame", nil, parent)
frame:SetAllPoints(parent)
region.frame = frame
local fs = frame:CreateFontString(nil, "OVERLAY")
region.fontString = fs
function region:Update()
WA.textCore.SetText(self.fontString,
WA.ReplacePlaceHolders(self.text or "", self.parent, self.formatters))
self:UpdateAnchorOnTextChange()
end
function region:SetFrameLevel(level) self.frame:SetFrameLevel(level) end
function region:Show() if self.visible ~= false then self.fontString:Show() end end
function region:Hide() self.fontString:Hide() end
function region:SetVisible(b)
self.visible = b
if b then self.fontString:Show() else self.fontString:Hide() end
end
function region:SetTextColor(r, g, b, a)
self.fontString:SetTextColor(r, g, b, a or 1)
end
-- Through textCore, not a bare SetTextHeight: a size change has to re-enter
-- SetFont's read-back, and textCore is what pairs the two the client's size
-- ceiling needs.
function region:SetTextHeight(size)
WA.textCore.Apply(self.fontString, self.subData, "text_", size, WA.textCore.SUBTEXT_KEYS)
end
-- One stable reference per instance rather than a fresh closure per
-- modify: RemoveSubscriber matches by identity, so a tick subscribed as an
-- anonymous function can be added but never taken off again -- which is
-- what ChangeText below needs to do when a %p string is swapped out.
region.frameTick = function() region:Update() end
-- Two independent reasons to repaint per frame: the string's own (%p, or
-- an every-frame formatter), and a %c whose function runs per frame. The
-- second is the parent's setting, asked one level up. Re-derived rather
-- than decided once at modify, because a condition can swap the whole
-- string afterwards: one that gains a %p has to start ticking, one that
-- loses it has to stop.
function region:ConfigureTick()
local host = self.parent
host.subRegionEvents:RemoveSubscriber("FrameTick", self.frameTick)
local needsTick = WA.TextNeedsFrameTick(self.text, self.everyFrameFormatters)
or (host.customTextFunc ~= nil and host.customTextMode == "update"
and WA.ContainsCustomPlaceHolder(self.text))
if needsTick then
host.subRegionEvents:AddSubscriber("FrameTick", self.frameTick)
end
proto.RefreshFrameTick(host)
end
function region:ChangeText(msg)
self.text = msg or ""
self:ConfigureTick()
self:Update()
end
-- Offsets are re-applied through the shared anchor resolver rather than a
-- bare SetPoint: it owns which target and which points this sub-region is
-- anchored against, and only the two numbers are being overridden.
function region:ApplyAnchor()
local effective = self.subData
if self.xOffset ~= nil or self.yOffset ~= nil then
effective = {}
for k, v in pairs(self.subData) do effective[k] = v end
if self.xOffset ~= nil then effective.anchorXOffset = self.xOffset end
if self.yOffset ~= nil then effective.anchorYOffset = self.yOffset end
end
local xo, yo = rotateOffset(self.fontString, self.rotateText,
self.anchorDefaults and self.anchorDefaults.selfPoint or "CENTER")
if xo ~= 0 or yo ~= 0 then
if effective == self.subData then
effective = {}
for k, v in pairs(self.subData) do effective[k] = v end
end
effective.anchorXOffset = (effective.anchorXOffset or 0) + xo
effective.anchorYOffset = (effective.anchorYOffset or 0) + yo
end
proto.AnchorSubRegion(self.fontString, self.parent, effective, self.anchorDefaults)
end
function region:SetXOffset(v) self.xOffset = v; self:ApplyAnchor() end
function region:SetYOffset(v) self.yOffset = v; self:ApplyAnchor() end
-- The offset depends on the string's own measured box, so a rotated
-- sub-region has to re-anchor whenever its text changes -- upstream wires
-- the same call as UpdateAnchorOnTextChange. Unrotated, the offset is
-- always 0,0 and re-anchoring would be pure cost, so it is skipped.
function region:UpdateAnchorOnTextChange()
if self.rotateText and self.rotateText ~= "NONE" then self:ApplyAnchor() end
end
function region:SetRotateText(v)
self.rotateText = v or "NONE"
if WA.hasInlineText then
self.fontString:SetRotation(ROTATE_RADIANS[self.rotateText] or 0)
end
self:ApplyAnchor()
end
return region
end,
modify = function(parent, region, parentData, subData)
region.subData = subData
region.text = subData.text_text or ""
region.visible = subData.text_visible ~= false
-- Config is the source of truth again on every modify; a condition that
-- is still active re-applies its override immediately afterwards.
region.xOffset, region.yOffset = nil, nil
region.formatters, region.everyFrameFormatters =
WA.CreateFormatters(subTexts(parentData, subData), formatGetter(subData), parentData)
local fs = region.fontString
WA.textCore.Apply(fs, subData, "text_", nil, WA.textCore.SUBTEXT_KEYS)
region.anchorDefaults = {
mode = subData.anchor_mode or "point", target = parent.subRegionAnchor and "bar" or "region",
anchorPoint = subData.anchor_point or "CENTER",
selfPoint = subData.anchor_point or "CENTER",
x = subData.anchorXOffset or 0, y = subData.anchorYOffset or 0,
}
region:SetRotateText(subData.rotateText)
if region.visible then fs:Show() else fs:Hide() end
parent.subRegionEvents:AddSubscriber("Update", function() region:Update() end)
parent.subRegionEvents:AddSubscriber("PreShow", function()
if region.visible then fs:Show() end
end)
-- The parent has already subscribed its own %c refresh ahead of this, so
-- what a tick leaves to do here is re-resolve the string against the
-- values it just recomputed.
region:ConfigureTick()
region:Update()
end,
-- BuildOptions field array for OptionsFrame's Display Effects list.
options = function(parentData, subData, index)
local fields = {
{
type = "input", name = WA.TextSymbolLabel("Text"), key = "text_text",
get = function() return subData.text_text end,
-- Re-renders the tab: Format Options below is one row per symbol
-- in this string, so editing it changes which rows exist.
set = function(v)
subData.text_text = v
WA.SetDefaultFormatters(subData.text_text, formatGetter(subData),
function(key, value) subData["text_text_format_" .. key] = value end,
parentData)
WA.Add(parentData, true)
WA.RefreshOptions()
end,
},
{
type = "range", name = "Size", key = "text_fontSize", min = 6, softMax = 72, step = 1, half = true,
get = function() return subData.text_fontSize end,
set = function(v) subData.text_fontSize = v; WA.Add(parentData, true) end,
},
{
type = "color", name = "Color", key = "text_color",
get = function() return subData.text_color end,
set = function(v) subData.text_color = v; WA.Add(parentData, true) end,
},
}
-- Offered only where the client can turn text: the option would otherwise
-- store a setting nothing acts on. Rotation rides WA.hasInlineText, so
-- this disappears on the same client that loses inline icons.
if WA.hasInlineText then
table.insert(fields, {
type = "select", name = "Rotate Text", key = "rotateText", half = true,
values = WA.text_rotate_values, labels = WA.text_rotate_types,
get = function() return subData.rotateText or "NONE" end,
set = function(v) subData.rotateText = v; WA.Add(parentData, true) end,
})
end
local anchorFields = WA.regionPrototype.SubRegionAnchorFields(parentData, subData, {
mode = subData.anchor_mode or "point", target = parentData.regionType == "progressbar" and "bar" or "region",
anchorPoint = subData.anchor_point or "CENTER",
selfPoint = subData.anchor_point or "CENTER",
x = subData.anchorXOffset or 0, y = subData.anchorYOffset or 0,
})
for i = 1, #anchorFields do table.insert(fields, anchorFields[i]) end
-- Directly under the Text field, whose label can only name the five
-- built-in symbols.
local hint = WA.TextSymbolHint(parentData)
if hint then table.insert(fields, 2, { type = "description", name = hint }) end
-- Keyed inside the "sub:" namespace for the same reason the Format fold
-- below is: removing a sub-region renumbers the rest, and clearCollapsed
-- drops the whole namespace with it.
local fontFields = WA.textCore.OptionFields(parentData, "sub:" .. index .. ":font",
function(key) return subData["text_" .. key] end,
function(key, v)
subData["text_" .. key] = v
WA.Add(parentData, true)
end, WA.textCore.SUBTEXT_KEYS)
for i = 1, #fontFields do table.insert(fields, fontFields[i]) end
local get = formatGetter(subData)
local formatFields = WA.FormatOptionFields(subData.text_text, get,
function(key, v)
subData["text_text_format_" .. key] = v
WA.Add(parentData, true)
end, parentData)
if #formatFields > 0 then
-- Folded away by default: every %symbol in the text grows a Format row,
-- and with two or three symbols those rows bury the ones actually being
-- edited under settings that get chosen once. The summary keeps the
-- closed line honest about what they hold.
--
-- Keyed inside the "sub:" namespace so removing a sub-region clears this
-- along with that sub-region's own fold state (OptionsFrame's
-- clearCollapsed) -- otherwise the renumbering would land it on a
-- different text's format rows.
local S = WA.OptionsState
local key = "sub:" .. index .. ":format"
local collapsed = S.isCollapsed(parentData, key, true)
table.insert(fields, {
type = "disclosure", name = "Format Options",
summary = WA.FormatSummary(subData.text_text, get, parentData),
collapsed = collapsed,
onToggle = function()
S.setCollapsed(parentData, key, not collapsed)
WA.RefreshOptions()
end,
})
if not collapsed then
for i = 1, #formatFields do
table.insert(fields, formatFields[i])
end
end
end
return fields
end,
})