Vampify 0.3.0
A TurtleWoW 1.12.1 addon that shows the healing returned by the Vampirism item stat. The game emits no event for that healing, so the addon computes it from the player's own outgoing damage using a formula measured in-game, and shows it live on a movable bar with a per-ability breakdown, an overheal split, automatic source detection from equipped gear, and optional scrolling combat text.
This commit is contained in:
+4028
File diff suppressed because it is too large
Load Diff
+35
@@ -0,0 +1,35 @@
|
||||
-- Vampify -- optional floating combat text, via Blizzard's own Blizzard_CombatText.
|
||||
--
|
||||
-- One of two SCT engines (the other is gui/sct.lua), selected by VampifyDB.sct.mode ==
|
||||
-- "blizzard" and dispatched from core/commands.lua. Blizzard's engine owns its own position and
|
||||
-- timing -- an addon cannot move or slow it down -- but CombatText_AddMessage takes an explicit
|
||||
-- r,g,b, so the user's chosen SCT color (VampifyDB.sct.color) applies here too. That is the one
|
||||
-- thing this engine and gui/sct.lua share.
|
||||
--
|
||||
-- There is no ambient FCT to hook: Blizzard_CombatText is LoadOnDemand ("## LoadOnDemand: 1") and
|
||||
-- off by default (SHOW_COMBAT_TEXT is a saved variable, not a CVar), and no SCT/MSBT is installed.
|
||||
-- So we write into Blizzard's FCT only if it is actually there, and are otherwise silent.
|
||||
--
|
||||
-- Signature verified against Blizzard 1.12.1 FrameXML, Blizzard_CombatText.lua:291 --
|
||||
-- CombatText_AddMessage(message, scrollFunction, r, g, b, displayType, isStaggered)
|
||||
-- The function does NOT itself honour SHOW_COMBAT_TEXT (that check lives in its event path), so we
|
||||
-- honour it here. COMBAT_TEXT_SCROLL_FUNCTION is only assigned during that addon's setup and can
|
||||
-- be nil even when the function exists, hence the fallback.
|
||||
--
|
||||
-- Increments of 0 are NOT emitted: the accumulator does not cross an integer on every hit. That is
|
||||
-- also what keeps the number rate readable without a merge rule -- six sources cannot produce six
|
||||
-- numbers per hit, because there is only ever one crossing.
|
||||
|
||||
VampifyFct = {}
|
||||
local F = VampifyFct
|
||||
|
||||
function F.emit(increment, isCrit)
|
||||
if not increment or increment <= 0 then return end
|
||||
local cfg = VampifyConfig.get()
|
||||
if not cfg or not cfg.sct or not cfg.sct.enabled or cfg.sct.mode ~= "blizzard" then return end
|
||||
if not CombatText_AddMessage then return end
|
||||
if SHOW_COMBAT_TEXT ~= "1" then return end
|
||||
local scroll = COMBAT_TEXT_SCROLL_FUNCTION or CombatText_StandardScroll
|
||||
local col = cfg.sct.color or { r = 0.4, g = 0.9, b = 0.4 }
|
||||
CombatText_AddMessage("+"..increment, scroll, col.r, col.g, col.b)
|
||||
end
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
-- Vampify -- minimap button, ported from DopingControl's features/minimap.lua as closely as
|
||||
-- possible: that file's shape is in-game verified on 1.12, and its comments below carry facts that
|
||||
-- must not be changed on the way over.
|
||||
--
|
||||
-- Design facts (in-game verified on 1.12, DopingControl):
|
||||
-- - Hand-rolled, no LibDBIcon on 1.12.
|
||||
-- - pfUI-safe: parent = Minimap, frame NAME contains "Minimap", SetFrameStrata("HIGH") +
|
||||
-- SetFrameLevel(9) -- MEDIUM hides under pfUI (the exact failure this addon's own display frame
|
||||
-- hit at strata MEDIUM, see gui/display.lua).
|
||||
-- - Border recipe (vanilla FrameXML MiniMapTrackingButton): 54px MiniMap-TrackingBorder anchored
|
||||
-- TOPLEFT(0,0) on a 33px button. The ring is deliberately off-centre within the texture -- do NOT
|
||||
-- centre it.
|
||||
-- - Drag + click coexist: drag flag on OnDragStart/Stop, plain clicks still fire OnClick.
|
||||
-- - Radius = Minimap:GetWidth()/2 + 5 (follows resized minimaps).
|
||||
-- - Highlight texture is "UI-Minimap-ZoomButton-Highlight" WITH the hyphen before "Highlight" --
|
||||
-- confirmed against FrameXML-1.12.1/FrameXML/Minimap.xml. The un-hyphenated spelling does not
|
||||
-- exist as a file and would render nothing.
|
||||
--
|
||||
-- Left click toggles the display frame (same code path as the "Show display" checkbox in
|
||||
-- gui/options.lua -- one definition of "is it on", the frame's own IsShown()); right click opens
|
||||
-- the options window.
|
||||
--
|
||||
-- Pure-Lua 5.0 dofile-loadable: all WoW wiring behind "if CreateFrame then"; BuildButton/PlaceButton
|
||||
-- only touch the WoW API when called at runtime.
|
||||
|
||||
VampifyMinimap = {}
|
||||
local M = VampifyMinimap
|
||||
|
||||
local button = nil
|
||||
|
||||
-- NOTE, measured in-game 2026-08-10 on this client: pfUI COLLECTS addon minimap buttons into its
|
||||
-- own `pfMinimapButtons` panel -- it reparents the button and owns its anchor from then on. So on a
|
||||
-- pfUI setup the angle below (and dragging around the ring) has no effect, and that is fine: the
|
||||
-- collected panel is where a pfUI user expects to find addon buttons. The code stays because it is
|
||||
-- what places the button correctly WITHOUT pfUI. Do not "fix" the placement by fighting pfUI for
|
||||
-- the anchor.
|
||||
--
|
||||
-- Corollary for anyone debugging position here: never compare GetLeft/GetCenter across frames
|
||||
-- without converting through GetEffectiveScale first. Once pfUI adopts the button, its effective
|
||||
-- scale (0.47) differs from the Minimap's (0.71), and raw coordinates look wildly wrong while the
|
||||
-- button sits exactly where it should.
|
||||
local function PlaceButton(angleDeg)
|
||||
if not button then return end
|
||||
local radius = (Minimap:GetWidth() / 2) + 5
|
||||
local rad = (angleDeg or 0) * math.pi / 180
|
||||
button:ClearAllPoints()
|
||||
button:SetPoint("CENTER", Minimap, "CENTER", math.cos(rad) * radius, math.sin(rad) * radius)
|
||||
end
|
||||
|
||||
-- ---- WoW wiring ------------------------------------------------------------------------------
|
||||
|
||||
if CreateFrame then
|
||||
|
||||
-- The frame's own IsShown() answers "is it on screen right now", which is what a toggle has to
|
||||
-- flip -- but it is NOT where the answer is kept. core/commands.lua re-decides visibility on
|
||||
-- every loading screen and every gear scan, so a hide that lived only in the frame lasted until
|
||||
-- the next portal. The click therefore records the intent in VampifyDB.shown (the "Show
|
||||
-- display" checkbox in gui/options.lua reads that same field, so the two cannot drift) and acts
|
||||
-- on the frame afterwards.
|
||||
function M.toggleDisplay()
|
||||
local f = getglobal("VampifyDisplayFrame")
|
||||
local on = not (f and f:IsShown())
|
||||
local cfg = VampifyConfig.get()
|
||||
if cfg then cfg.shown = on end
|
||||
if on then VampifyDisplay.show() else VampifyDisplay.hide() end
|
||||
end
|
||||
|
||||
local function BuildButton()
|
||||
if button then return end
|
||||
local btn = CreateFrame("Button", "VampifyMinimapButton", Minimap)
|
||||
btn:SetWidth(33); btn:SetHeight(33)
|
||||
btn:SetFrameStrata("HIGH"); btn:SetFrameLevel(9)
|
||||
|
||||
local icon = btn:CreateTexture("VampifyMinimapIcon", "ARTWORK")
|
||||
icon:SetTexture("Interface\\Icons\\Spell_Shadow_LifeDrain02")
|
||||
icon:SetWidth(20); icon:SetHeight(20)
|
||||
icon:SetPoint("CENTER", btn, "CENTER", 0, 0)
|
||||
icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) -- standard action-button crop
|
||||
|
||||
local border = btn:CreateTexture("VampifyMinimapBorder", "OVERLAY")
|
||||
border:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder")
|
||||
border:SetWidth(54); border:SetHeight(54)
|
||||
border:SetPoint("TOPLEFT", btn, "TOPLEFT", 0, 0)
|
||||
|
||||
btn:SetHighlightTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight")
|
||||
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
||||
btn:RegisterForDrag("LeftButton")
|
||||
|
||||
btn:SetScript("OnDragStart", function() this.isDragging = true end)
|
||||
btn:SetScript("OnDragStop", function() this.isDragging = false end)
|
||||
btn:SetScript("OnUpdate", function()
|
||||
if not this.isDragging then return end
|
||||
local mx, my = Minimap:GetCenter()
|
||||
local cx, cy = GetCursorPosition()
|
||||
local scale = Minimap:GetEffectiveScale()
|
||||
cx, cy = cx / scale, cy / scale
|
||||
-- atan2, computed by hand rather than trusting math.deg to be present: the
|
||||
-- VampifyDB.mmAngle contract downstream (PlaceButton) is degrees, so this returns
|
||||
-- degrees too.
|
||||
local angle = math.atan2(cy - my, cx - mx) * 180 / math.pi
|
||||
local cfg = VampifyConfig.get()
|
||||
if cfg then cfg.mmAngle = angle end -- persist live while dragging
|
||||
PlaceButton(angle)
|
||||
end)
|
||||
|
||||
-- LEFT = display toggle, RIGHT = options.
|
||||
btn:SetScript("OnClick", function()
|
||||
if arg1 == "RightButton" then
|
||||
VampifyOptions.toggle()
|
||||
else
|
||||
M.toggleDisplay()
|
||||
end
|
||||
end)
|
||||
btn:SetScript("OnEnter", function()
|
||||
GameTooltip:SetOwner(this, "ANCHOR_LEFT")
|
||||
GameTooltip:SetText("Vampify", 1, 1, 1)
|
||||
local fight = VampifyAggregate.fight(VampifyState)
|
||||
GameTooltip:AddLine(string.format("%d healed this fight, %.1f%%",
|
||||
math.floor(fight.heal or 0), (fight.pct or 0) * 100), 0.9, 0.9, 0.9)
|
||||
GameTooltip:AddLine("Left click: show / hide the display", 0.8, 0.8, 0.8)
|
||||
GameTooltip:AddLine("Right click: options", 0.8, 0.8, 0.8)
|
||||
GameTooltip:AddLine("Drag: move around the minimap", 0.8, 0.8, 0.8)
|
||||
GameTooltip:Show()
|
||||
end)
|
||||
btn:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
||||
|
||||
button = btn
|
||||
local cfg = VampifyConfig.get()
|
||||
PlaceButton((cfg and cfg.mmAngle) or VampifyConfig.DEFAULTS.mmAngle)
|
||||
end
|
||||
|
||||
-- Honours the "Minimap button" checkbox (VampifyDB.minimap); gui/options.lua calls this after
|
||||
-- toggling the setting.
|
||||
function M.update()
|
||||
local cfg = VampifyConfig.get()
|
||||
if not cfg then return end
|
||||
if not cfg.minimap then
|
||||
if button then button:Hide() end
|
||||
return
|
||||
end
|
||||
BuildButton()
|
||||
PlaceButton(cfg.mmAngle)
|
||||
button:Show()
|
||||
end
|
||||
|
||||
end
|
||||
+806
@@ -0,0 +1,806 @@
|
||||
-- Vampify -- options window, built in DopingControl's visual language (gui/widgets.lua) so the two
|
||||
-- addons read as the same toolkit rather than as a Blizzard dialog bolted onto a hand-rolled one.
|
||||
--
|
||||
-- Layout follows DopingControl's ui/options.lua pattern: a lazily-built frame, a layout cursor that
|
||||
-- places widgets top-down, checkboxes with getter/setter pairs repopulated on OnShow, buttons via
|
||||
-- VampifyWidgets.MakeButton, and a throttled info panel that only repaints text that changed
|
||||
-- (VampifyWidgets.SetTextG). The info panel itself is specific to this addon -- there is no grid
|
||||
-- here, just the source list and the honesty lines about what the number does and does not mean.
|
||||
--
|
||||
-- Pure-Lua 5.0 dofile-loadable: all WoW wiring behind "if CreateFrame then".
|
||||
|
||||
VampifyOptions = {}
|
||||
local O = VampifyOptions
|
||||
|
||||
-- ---- WoW wiring ------------------------------------------------------------------------------
|
||||
|
||||
if CreateFrame then
|
||||
local W = VampifyWidgets
|
||||
local C = W.COLORS
|
||||
|
||||
local frame
|
||||
local checkboxes = {}
|
||||
local infoFS
|
||||
local elapsed = 0
|
||||
local shareBuf = {}
|
||||
|
||||
-- SCT color/position controls -- declared here (not local to build()) so refreshSwatch() and
|
||||
-- refreshEnabled() below can reach them from OnShow/OnUpdate as well as from the checkboxes'
|
||||
-- own setters. sliderPillH/sliderPanelBarH (2026-08-25) join the same list for the same reason
|
||||
-- -- refreshSliders() below needs to push their persisted values in on OnShow, same as
|
||||
-- sliderFont/sliderRise already do.
|
||||
local swatch, btnMove, btnPosReset, posLabel, sliderFont, sliderRise, btnDemo, btnOutline,
|
||||
sliderPillH, sliderPanelBarH
|
||||
|
||||
local function db() return VampifyConfig.get() end
|
||||
|
||||
-- Same merge as core/commands.lua's recompute(): detected sources plus manual overrides. Kept
|
||||
-- here rather than reading commands.lua's private locals (there is nothing exported to read),
|
||||
-- so the info panel's Total/Floor line matches what the addon actually uses to size a hit.
|
||||
local function totals()
|
||||
local sources = VampifyDetect.getSources()
|
||||
local sum, n = VampifyDetect.summarise(sources)
|
||||
local cfg = VampifyConfig.getChar()
|
||||
if cfg and cfg.manual then
|
||||
for i = 1, table.getn(cfg.manual) do
|
||||
sum = sum + (cfg.manual[i] / 100)
|
||||
n = n + 1
|
||||
end
|
||||
end
|
||||
return sources, sum, n
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- widget helpers
|
||||
-- ------------------------------------------------------------------
|
||||
local checkCounter = 0
|
||||
local function CreateCheckbox(parent, label, getter, setter)
|
||||
checkCounter = checkCounter + 1
|
||||
local name = "VampifyOptionsCheck" .. checkCounter
|
||||
local cb = CreateFrame("CheckButton", name, parent, "UICheckButtonTemplate")
|
||||
cb:SetWidth(24)
|
||||
cb:SetHeight(24)
|
||||
local text = getglobal(name .. "Text")
|
||||
text:SetText(label)
|
||||
W.ApplyFont(text, 11)
|
||||
W.SetTextColorG(text, C.ink)
|
||||
cb.vfLabelFS = text -- exposed so a caller can chain another widget off the label's
|
||||
-- right edge (measured, not estimated -- see the SCT engine row)
|
||||
cb.vfGet = getter
|
||||
cb.vfSet = setter
|
||||
cb:SetScript("OnClick", function()
|
||||
this.vfSet(this:GetChecked() == 1) -- GetChecked returns 1 | nil
|
||||
end)
|
||||
table.insert(checkboxes, cb)
|
||||
return cb
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- info panel text -- rebuilt only when this fires (throttled OnUpdate), never per damage event
|
||||
-- ------------------------------------------------------------------
|
||||
local infoLines = {}
|
||||
local floorBuf = {} -- reused result table for the floor-share line below
|
||||
|
||||
-- The upgrade verdict, THROTTLED to once a second and cached as a finished string.
|
||||
--
|
||||
-- WHY THROTTLED RATHER THAN COMPUTED ONCE ON OPEN. VampifyHistogram.compare is four passes over
|
||||
-- the whole recorded distribution -- the baseline plus three hypotheticals -- and this panel
|
||||
-- repaints twice a second for as long as it is open, so computing it per repaint would spend
|
||||
-- eight passes a second on a line whose answer moves about as fast as the player's gear does.
|
||||
-- But once-on-open would be worse in the case that matters: the panel is left open while
|
||||
-- fighting, the distribution keeps filling, and a verdict frozen at the moment of opening would
|
||||
-- be stale exactly when the player is watching it change. Once a second is two orders of
|
||||
-- magnitude cheaper than the alternative and still live.
|
||||
--
|
||||
-- Everything is held in ONE table rather than three locals on purpose: this file's enclosing
|
||||
-- closure is already wide, and Lua 5.0 caps locals and upvalues per function.
|
||||
local cmp = { buf = {}, text = nil, at = nil }
|
||||
local function upgradeLine()
|
||||
local now = GetTime and GetTime() or nil
|
||||
-- No GetTime (offline/stub): compute once and keep it, rather than every repaint.
|
||||
if cmp.text and (not now or (cmp.at and (now - cmp.at) < 1)) then return cmp.text end
|
||||
cmp.at, cmp.text = now, ""
|
||||
if not (VampifyHistogram and VampifyState and VampifyState.hist) then return cmp.text end
|
||||
-- Always at +1 point here. The panel states the CHOICE, not a shopping calculator; a player
|
||||
-- who wants a different step has /vf upgrade <pct>, which prints both scopes as well.
|
||||
local c = VampifyHistogram.compare(VampifyState.hist, VampifyState.sourcePercents, 1, cmp.buf)
|
||||
if not (c.hits and c.hits > 0) then return cmp.text end
|
||||
if c.winner == "swap" and c.swapIndex then
|
||||
cmp.text = string.format("Upgrade: swap your weakest source (%d%% -> %d%%) - worth"
|
||||
.." +%d HP here against +%d for one more 1%% source",
|
||||
c.swapFrom, c.swapTo, math.floor(c.swapDelta), math.floor(c.addDelta))
|
||||
elseif c.winner == "add" then
|
||||
cmp.text = string.format("Upgrade: one more 1%% source - worth +%d HP here against"
|
||||
.." +%d for swapping your weakest one",
|
||||
math.floor(c.addDelta), math.floor(c.swapDelta))
|
||||
else
|
||||
cmp.text = string.format("Upgrade: one more 1%% source and swapping your weakest one"
|
||||
.." are worth the same here (+%d HP each)", math.floor(c.addDelta))
|
||||
end
|
||||
return cmp.text
|
||||
end
|
||||
|
||||
local function composeInfo()
|
||||
VampifyConst.resetList(infoLines) -- nil-ing indices is not enough in 5.0; see const.lua
|
||||
|
||||
local sources, sumPercent, nSources = totals()
|
||||
local n = table.getn(sources)
|
||||
for i = 1, n do
|
||||
table.insert(infoLines, string.format("Slot %d: %d%% (%s)",
|
||||
sources[i].slot, sources[i].percent, sources[i].kind))
|
||||
end
|
||||
table.insert(infoLines, string.format("Total: %.0f%% from %d sources", sumPercent * 100, nSources))
|
||||
table.insert(infoLines, string.format("Floor: %d HP per hit", nSources))
|
||||
-- How much of this session's healing the floor ACTUALLY paid, over the recorded hit
|
||||
-- distribution (core/histogram.lua) rather than as a per-hit minimum. A high share means
|
||||
-- the hits are small enough that every source is pinned at 1 HP, which is the regime where
|
||||
-- one MORE source beats swapping a weak one out. One pass over the distribution, cheap
|
||||
-- enough to run on every repaint; the verdict line below it is the expensive part and is
|
||||
-- throttled separately (see upgradeLine).
|
||||
if VampifyHistogram and VampifyState and VampifyState.hist then
|
||||
local t = VampifyHistogram.totals(VampifyState.hist, VampifyState.sourcePercents, floorBuf)
|
||||
if t.hits > 0 then
|
||||
table.insert(infoLines, string.format(
|
||||
"Floor share: %d of %d HP (%.0f%%) over %d hits",
|
||||
math.floor(t.floorHeal), math.floor(t.heal), t.floorShare * 100, t.hits))
|
||||
end
|
||||
-- The decision itself, so it is readable without typing anything. /vf upgrade still
|
||||
-- gives the full report: both scopes, a chosen step size, and the ceiling figure.
|
||||
local up = upgradeLine()
|
||||
if up ~= "" then table.insert(infoLines, up) end
|
||||
end
|
||||
|
||||
local fight = VampifyAggregate.fight(VampifyState)
|
||||
local session = VampifyAggregate.session(VampifyState)
|
||||
local eff = VampifyState.effHeal or 0
|
||||
local overheal = (fight.heal or 0) - eff
|
||||
if overheal < 0 then overheal = 0 end
|
||||
local ohPct = 0
|
||||
if fight.heal and fight.heal > 0 then ohPct = overheal / fight.heal * 100 end
|
||||
|
||||
table.insert(infoLines, string.format("Fight: %d healed, %.2f%%",
|
||||
math.floor(fight.heal or 0), (fight.pct or 0) * 100))
|
||||
-- Overheal is CALCULATED from the health deficit at hit time, not measured -- Vampirism
|
||||
-- emits no heal event. The line right below says so; do not let the number alone imply
|
||||
-- more precision than that.
|
||||
table.insert(infoLines, string.format("Effective: %d healed, %d overheal (%.0f%%)",
|
||||
math.floor(eff), math.floor(overheal), ohPct))
|
||||
table.insert(infoLines, "|cff" .. string.format("%02x%02x%02x",
|
||||
C.faint.r * 255, C.faint.g * 255, C.faint.b * 255)
|
||||
.. "Overheal is derived from the health deficit at the time of the hit, not from a"
|
||||
.. " heal event - Vampirism emits none.|r")
|
||||
table.insert(infoLines, string.format("Session: %d healed, %.2f%%",
|
||||
math.floor(session.heal or 0), (session.pct or 0) * 100))
|
||||
|
||||
local flag = VampifyDisplay.boundFlag(VampifyConst.FORMULA.channels, VampifyDamage.aoeAvailable())
|
||||
if flag == "lower" or flag == "uncertain" then
|
||||
table.insert(infoLines, "|cffe0c98aLower bound: DoT and PvP damage are unmeasured and excluded.|r")
|
||||
end
|
||||
if flag == "upper" or flag == "uncertain" then
|
||||
table.insert(infoLines, "|cffe0c98aUpper bound: no SPELL_GO seen, AoE damping not applied.|r")
|
||||
end
|
||||
if not VampifyDamage.damageSeen() then
|
||||
table.insert(infoLines, "|cffd05252No damage events received - nampower may be absent.|r")
|
||||
end
|
||||
|
||||
return table.concat(infoLines, "\n")
|
||||
end
|
||||
|
||||
-- The info panel grows and shrinks with what it has to say: the source list is as long as the
|
||||
-- player's gear, and the honesty lines come and go. A fixed frame height therefore cannot fit
|
||||
-- it -- reported in-game as text hanging out of the bottom edge.
|
||||
--
|
||||
-- Note the panel is measured, not estimated: 1.12 has no GetStringHeight (only GetStringWidth
|
||||
-- exists in Blizzard's own FrameXML), but a FontString with an explicit width lays its wrapped
|
||||
-- text out and reports the real height through GetHeight().
|
||||
local function fitHeight()
|
||||
if not (frame and infoFS) then return end
|
||||
local top, iTop, iH = frame:GetTop(), infoFS:GetTop(), infoFS:GetHeight()
|
||||
if not (top and iTop and iH) then return end
|
||||
local need = (top - iTop) + iH + 18 -- header block + text + bottom padding
|
||||
if need < 300 then need = 300 end -- never collapse to nothing
|
||||
if math.abs(frame:GetHeight() - need) > 2 then frame:SetHeight(need) end
|
||||
end
|
||||
|
||||
local function refreshInfo()
|
||||
if infoFS then
|
||||
W.SetTextG(infoFS, composeInfo())
|
||||
fitHeight()
|
||||
end
|
||||
end
|
||||
|
||||
local function refreshChecks()
|
||||
local cfg = db()
|
||||
if not cfg then return end
|
||||
for i = 1, table.getn(checkboxes) do
|
||||
local cb = checkboxes[i]
|
||||
cb:SetChecked(cb.vfGet() and 1 or nil)
|
||||
end
|
||||
end
|
||||
|
||||
-- Swatch background = the currently configured SCT color. Applies in both modes (see
|
||||
-- gui/fct.lua), so it is never part of refreshEnabled()'s greying below.
|
||||
local function refreshSwatch()
|
||||
if not swatch then return end
|
||||
local cfg = db()
|
||||
local col = cfg and cfg.sct and cfg.sct.color
|
||||
if col then swatch:SetBackdropColor(col.r, col.g, col.b, 1) end
|
||||
end
|
||||
|
||||
-- Greys out the controls that are genuinely inert outside "Free anchor" mode: Blizzard mode
|
||||
-- owns its own position and timing (Blizzard_CombatText), and bar mode derives its position
|
||||
-- from VampifyDisplayFrame rather than the stored free position -- in both cases the slider/
|
||||
-- Move/Reset controls below have nothing to act on. NOT the color swatch, which still applies
|
||||
-- in all three modes.
|
||||
local function refreshEnabled()
|
||||
local cfg = db()
|
||||
local mode = cfg and cfg.sct and cfg.sct.mode
|
||||
-- Active only in "own" -- nil (fresh/unmigrated profile) and any unrecognised value fall
|
||||
-- back to enabled too, matching gui/sct.lua's resolveAnchorMode's "unknown -> free" rule.
|
||||
local posEnabled = (mode ~= "bar" and mode ~= "blizzard")
|
||||
|
||||
if btnMove then W.SetButtonEnabled(btnMove, posEnabled) end
|
||||
if btnPosReset then W.SetButtonEnabled(btnPosReset, posEnabled) end
|
||||
if posLabel then W.SetTextColorG(posLabel, posEnabled and C.ink or C.faint) end
|
||||
-- The sliders shape THIS addon's own SCT only; Blizzard's owns its font and travel.
|
||||
-- NOT Enable()/Disable(): a Slider has neither in 1.12 (it threw in-game). See
|
||||
-- W.SetSliderEnabled, which follows Blizzard's own hide-the-thumb recipe.
|
||||
W.SetSliderEnabled(sliderRise, posEnabled)
|
||||
W.SetSliderEnabled(sliderFont, posEnabled)
|
||||
if btnDemo then W.SetButtonEnabled(btnDemo, posEnabled) end
|
||||
-- The outline shapes OUR numbers only; Blizzard's engine draws its own.
|
||||
if btnOutline then W.SetButtonEnabled(btnOutline, posEnabled) end
|
||||
end
|
||||
|
||||
-- Push stored values into the sliders without letting OnValueChanged write them straight back.
|
||||
local function refreshSliders()
|
||||
if sliderFont then
|
||||
local v = sliderFont.vfGetter()
|
||||
sliderFont:SetValue(v)
|
||||
getglobal("VampifyOptionsFontSliderText"):SetText(sliderFont.vfLabel .. ": " .. v)
|
||||
end
|
||||
if sliderRise then
|
||||
local v = sliderRise.vfGetter()
|
||||
sliderRise:SetValue(v)
|
||||
getglobal("VampifyOptionsRiseSliderText"):SetText(sliderRise.vfLabel .. ": " .. v)
|
||||
end
|
||||
if sliderPillH then
|
||||
local v = sliderPillH.vfGetter()
|
||||
sliderPillH:SetValue(v)
|
||||
getglobal("VampifyOptionsPillHeightSliderText"):SetText(sliderPillH.vfLabel .. ": " .. v)
|
||||
end
|
||||
if sliderPanelBarH then
|
||||
local v = sliderPanelBarH.vfGetter()
|
||||
sliderPanelBarH:SetValue(v)
|
||||
getglobal("VampifyOptionsPanelBarHeightSliderText"):SetText(sliderPanelBarH.vfLabel .. ": " .. v)
|
||||
end
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- frame
|
||||
-- ------------------------------------------------------------------
|
||||
local function build()
|
||||
if frame then return frame end
|
||||
|
||||
local f = CreateFrame("Frame", "VampifyOptionsFrame", UIParent)
|
||||
f:SetWidth(340)
|
||||
f:SetHeight(540)
|
||||
f:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
|
||||
-- HIGH, deliberately NOT DIALOG. Blizzard's ColorPickerFrame lives at DIALOG, and a frame
|
||||
-- created later in the same strata draws on top of it -- so at DIALOG this window buried
|
||||
-- the very colour picker it opens (reported in-game). HIGH keeps us above the normal UI
|
||||
-- while leaving DIALOG to Blizzard's own popups, which is where they belong.
|
||||
f:SetFrameStrata("HIGH")
|
||||
f:SetBackdrop(W.PANEL_BACKDROP)
|
||||
f:SetBackdropColor(1, 1, 1, 0.97)
|
||||
f:SetMovable(true)
|
||||
f:EnableMouse(true)
|
||||
f:SetClampedToScreen(true)
|
||||
f:RegisterForDrag("LeftButton")
|
||||
f:SetScript("OnDragStart", function() this:StartMoving() end)
|
||||
f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
|
||||
|
||||
-- title (gold), version alongside it
|
||||
local title = W.MakeText(f, 14, C.goldHi, "ARTWORK")
|
||||
title:SetPoint("TOPLEFT", f, "TOPLEFT", 16, -16)
|
||||
title:SetText("Vampify v" .. VampifyConst.VERSION)
|
||||
|
||||
-- close X
|
||||
local closeX = CreateFrame("Button", "VampifyOptionsCloseX", f, "UIPanelCloseButton")
|
||||
closeX:SetPoint("TOPRIGHT", f, "TOPRIGHT", -8, -8)
|
||||
|
||||
-- layout cursor
|
||||
local x, y = 24, -46
|
||||
local function place(w, dy)
|
||||
w:SetPoint("TOPLEFT", f, "TOPLEFT", x, y)
|
||||
y = y + dy
|
||||
end
|
||||
|
||||
-- ---- checkboxes -------------------------------------------------------------------
|
||||
-- Reads the stored preference rather than the frame, so it cannot drift from the minimap
|
||||
-- button (which writes the same field) and cannot report "off" merely because the bar is
|
||||
-- currently down for a reason of its own -- no sources equipped, or the inert verdict.
|
||||
local cbShow = CreateCheckbox(f, "Show display",
|
||||
function() local cfg = db(); return cfg and cfg.shown end,
|
||||
function(v)
|
||||
local cfg = db()
|
||||
if cfg then cfg.shown = v end
|
||||
if v then VampifyDisplay.show() else VampifyDisplay.hide() end
|
||||
end)
|
||||
place(cbShow, -26)
|
||||
W.AttachTooltip(cbShow, "Same as left-clicking the minimap button.")
|
||||
|
||||
local cbLock = CreateCheckbox(f, "Lock display",
|
||||
function() local cfg = db(); return cfg and cfg.locked end,
|
||||
function(v) local cfg = db(); if cfg then cfg.locked = v end end)
|
||||
place(cbLock, -26)
|
||||
W.AttachTooltip(cbLock, "Prevents dragging the display frame around.")
|
||||
|
||||
local cbMinimap = CreateCheckbox(f, "Minimap button",
|
||||
function() local cfg = db(); return cfg and cfg.minimap end,
|
||||
function(v)
|
||||
local cfg = db()
|
||||
if cfg then cfg.minimap = v end
|
||||
if VampifyMinimap and VampifyMinimap.update then VampifyMinimap.update() end
|
||||
end)
|
||||
place(cbMinimap, -26)
|
||||
|
||||
local cbOverheal = CreateCheckbox(f, "Show overheal",
|
||||
function() local cfg = db(); return cfg and cfg.showOverheal end,
|
||||
function(v) local cfg = db(); if cfg then cfg.showOverheal = v end end)
|
||||
place(cbOverheal, -26)
|
||||
-- Tooltip text updated 2026-08-24 (Befund 2, pixel measurement pass): the compact bar no
|
||||
-- longer shows a "(N OH)" suffix at all (moved to the row-detail panel and the breakdown
|
||||
-- panel's Total line, to make room on an overflowing bar) -- this option's only remaining
|
||||
-- effect is the combat-text half, so the tooltip must not describe a bar effect that no
|
||||
-- longer exists.
|
||||
W.AttachTooltip(cbOverheal, "Spawns dimmed, parenthesized overheal numbers on the combat text.")
|
||||
|
||||
-- "Show vampirism % on bar" (2026-08-25, feature request: "one more: show vampirism % on
|
||||
-- bar - with total%/source count"). This existed on the bar once before ("14.0% (9 Gear)")
|
||||
-- and was cut entirely during the 340px->160px compaction pass -- it now comes back as an
|
||||
-- OPT-IN toggle instead of a permanent segment. DEFAULT OFF (per the original suggestion,
|
||||
-- taken as decided rather than merely offered): the bar stays at its established narrow
|
||||
-- width for every existing player, and nobody is surprised by it growing on the very next
|
||||
-- login -- the same "an existing profile sees zero visible change until it explicitly opts
|
||||
-- in" principle this file's own sliders (pillHeight/panelBarHeight, added at the same time)
|
||||
-- already follow. gui/display.lua's V.update() calls V.formatSources(VampifyState.sourcePercents)
|
||||
-- for the actual text -- the SAME figure this window's own info panel already shows further
|
||||
-- down ("Total: N% from M sources" -- composeInfo() above), fed by the same
|
||||
-- VampifyDetect.getSources() + manual overrides core/commands.lua's recompute() already
|
||||
-- merges into sourcePercents -- not a second computation.
|
||||
local cbGearPct = CreateCheckbox(f, "Show vampirism % on bar",
|
||||
function() local cfg = db(); return cfg and cfg.showGearPct end,
|
||||
function(v)
|
||||
local cfg = db()
|
||||
if cfg then cfg.showGearPct = v end
|
||||
-- Immediate effect, no /reload -- V.update() already runs the segment's own
|
||||
-- show/hide + text + pill-width re-layout logic every tick; calling it once here
|
||||
-- (rather than waiting up to ~0.5s for the next throttled tick) matches this file's
|
||||
-- own "sofort" convention for every other live-applied option/slider.
|
||||
if VampifyDisplay and VampifyDisplay.update then VampifyDisplay.update() end
|
||||
end)
|
||||
place(cbGearPct, -26)
|
||||
W.AttachTooltip(cbGearPct, "Adds \"<total%> (<source count>)\" to the bar -- the sum of your"
|
||||
.. " equipped gear's Vampirism percentages and how many sources make it up. Same figure"
|
||||
.. " as the \"Total\" line in this window's own info panel below. The bar grows to fit it.")
|
||||
|
||||
-- Same field (VampifyDB.perhitEnabled) /vf perhit on|off writes, core/commands.lua -- the
|
||||
-- two cannot drift, same as cbShow above and the minimap button's left-click both writing
|
||||
-- cfg.shown. The actual enable/disable side effect (not just the saved preference) is
|
||||
-- applied here directly, mirroring cbMinimap's direct VampifyMinimap.update() call below.
|
||||
local cbPerHit = CreateCheckbox(f, "Per-hit debug export",
|
||||
function() local cfg = db(); return cfg and cfg.perhitEnabled end,
|
||||
function(v)
|
||||
local cfg = db()
|
||||
if cfg then cfg.perhitEnabled = v end
|
||||
if not VampifyPerHit then return end
|
||||
if v then
|
||||
if not VampifyPerHit.isEnabled() then VampifyPerHit.enable() end
|
||||
elseif VampifyPerHit.isEnabled() then
|
||||
VampifyPerHit.disable(UnitHealth and UnitHealth("player"))
|
||||
end
|
||||
end)
|
||||
place(cbPerHit, -26)
|
||||
W.AttachTooltip(cbPerHit, "Writes one line per own hit to imports\\vampify_perhit_*.txt"
|
||||
.." for offline analysis (per-source Vampirism factor measurement). On by default.")
|
||||
|
||||
local cbSct = CreateCheckbox(f, "Scrolling combat text",
|
||||
function() local cfg = db(); return cfg and cfg.sct and cfg.sct.enabled end,
|
||||
function(v)
|
||||
local cfg = db()
|
||||
if cfg and cfg.sct then cfg.sct.enabled = v end
|
||||
end)
|
||||
place(cbSct, -26)
|
||||
|
||||
local cbCoalesce = CreateCheckbox(f, "One number per cast (not per hit)",
|
||||
function() local cfg = db(); return cfg and cfg.sct and cfg.sct.coalesce end,
|
||||
function(v)
|
||||
local cfg = db()
|
||||
if cfg and cfg.sct then cfg.sct.coalesce = v end
|
||||
end)
|
||||
place(cbCoalesce, -18)
|
||||
|
||||
local coalesceHint = W.MakeText(f, 9, C.faint, "ARTWORK")
|
||||
coalesceHint:SetPoint("TOPLEFT", f, "TOPLEFT", x + 26, y)
|
||||
coalesceHint:SetWidth(268)
|
||||
coalesceHint:SetJustifyH("LEFT")
|
||||
coalesceHint:SetText("An AoE hitting five targets shows its summed return once instead of"
|
||||
.. " five times. Totals are unaffected - this only changes what is drawn.")
|
||||
y = y - 26
|
||||
|
||||
-- ---- SCT engine (three mutually exclusive modes, VampifyDB.sct.mode) --------------------
|
||||
-- Not place()'s usual stacked column: three in a row reads as ONE choice, not three
|
||||
-- independent switches. A plain CheckButton has no native radio grouping in 1.12, so
|
||||
-- exclusivity is hand-rolled -- each setter writes cfg.sct.mode, then refreshChecks()
|
||||
-- resyncs every checkbox in the window from its own getter (checkboxes[] already holds all
|
||||
-- of them), which is also what snaps a click back to checked when it tried to uncheck the
|
||||
-- currently active mode (there is always exactly one -- "off" is not a fourth option here).
|
||||
local engineLabel = W.MakeText(f, 11, C.ink, "ARTWORK")
|
||||
place(engineLabel, -18)
|
||||
engineLabel:SetText("SCT engine")
|
||||
|
||||
local function setEngineMode(v)
|
||||
local cfg = db()
|
||||
if cfg and cfg.sct then cfg.sct.mode = v end
|
||||
-- Dragging the free anchor only means anything in "own" mode -- leaving move mode
|
||||
-- engaged while switching away would leave it silently unreachable from the (now grey)
|
||||
-- Move button, so it is force-released the same moment the mode stops being "own".
|
||||
if v ~= "own" and VampifySCT and VampifySCT.isMoveMode and VampifySCT.isMoveMode() then
|
||||
VampifySCT.setMoveMode(false)
|
||||
if btnMove then btnMove.text:SetText("Move") end
|
||||
end
|
||||
refreshChecks()
|
||||
refreshEnabled()
|
||||
-- Hangs the anchor immediately instead of waiting for the next SCT emit -- see
|
||||
-- gui/sct.lua's S.applyPos/reanchor comments for why a forced, explicit call is needed
|
||||
-- here on top of spawn()'s own lazy re-check.
|
||||
if VampifySCT and VampifySCT.applyPos then VampifySCT.applyPos() end
|
||||
end
|
||||
|
||||
local cbFree = CreateCheckbox(f, "Free anchor",
|
||||
function() local cfg = db(); local m = cfg and cfg.sct and cfg.sct.mode
|
||||
return m ~= "bar" and m ~= "blizzard" end,
|
||||
function(v) if v then setEngineMode("own") else refreshChecks() end end)
|
||||
place(cbFree, 0) -- dy 0: cbBar/cbBlizzard below share this row, y advances once after all three
|
||||
W.AttachTooltip(cbFree, "Vampify's own combat text on a draggable anchor you position anywhere.")
|
||||
|
||||
local cbBar = CreateCheckbox(f, "At the bar",
|
||||
function() local cfg = db(); return cfg and cfg.sct and cfg.sct.mode == "bar" end,
|
||||
function(v) if v then setEngineMode("bar") else refreshChecks() end end)
|
||||
cbBar:SetPoint("LEFT", cbFree.vfLabelFS, "RIGHT", 16, 0)
|
||||
W.AttachTooltip(cbBar, "Vampify's own combat text, anchored just above the display bar.")
|
||||
|
||||
local cbBlizzard = CreateCheckbox(f, "Blizzard FCT",
|
||||
function() local cfg = db(); return cfg and cfg.sct and cfg.sct.mode == "blizzard" end,
|
||||
function(v) if v then setEngineMode("blizzard") else refreshChecks() end end)
|
||||
cbBlizzard:SetPoint("LEFT", cbBar.vfLabelFS, "RIGHT", 16, 0)
|
||||
W.AttachTooltip(cbBlizzard, "Blizzard's own combat text (Blizzard_CombatText) - only the"
|
||||
.." colour is ours, position and timing are Blizzard's.")
|
||||
y = y - 26
|
||||
|
||||
local engineHint = W.MakeText(f, 9, C.faint, "ARTWORK")
|
||||
engineHint:SetPoint("TOPLEFT", f, "TOPLEFT", x, y)
|
||||
engineHint:SetWidth(292)
|
||||
engineHint:SetJustifyH("LEFT")
|
||||
engineHint:SetText("Free anchor and At the bar both use Vampify's own combat text below;"
|
||||
.." Blizzard FCT hands numbers to Blizzard's engine instead.")
|
||||
y = y - 24
|
||||
|
||||
-- ---- SCT color -----------------------------------------------------------------------
|
||||
swatch = CreateFrame("Button", "VampifyOptionsSctSwatch", f)
|
||||
swatch:SetWidth(16)
|
||||
swatch:SetHeight(16)
|
||||
swatch:SetPoint("TOPLEFT", f, "TOPLEFT", x, y)
|
||||
swatch:SetBackdrop(W.SOLID_BACKDROP)
|
||||
swatch:SetBackdropBorderColor(C.goldSoft.r, C.goldSoft.g, C.goldSoft.b, 1)
|
||||
|
||||
local swatchLabel = W.MakeText(f, 11, C.ink, "ARTWORK")
|
||||
swatchLabel:SetPoint("LEFT", swatch, "RIGHT", 8, 0)
|
||||
swatchLabel:SetText("SCT color")
|
||||
|
||||
-- Applies to both engines (gui/sct.lua and gui/fct.lua) -- never greyed by refreshEnabled().
|
||||
swatch:SetScript("OnClick", function()
|
||||
local cfg = db()
|
||||
if not (cfg and cfg.sct) then return end
|
||||
local c = cfg.sct.color
|
||||
|
||||
-- Order matters and is verified against Blizzard 1.12.1 FrameXML (UIDropDownMenu.lua,
|
||||
-- FloatingChatFrame.lua): ColorPickerFrame:SetColorRGB() FIRES .func immediately, so
|
||||
-- func/opacityFunc/cancelFunc/previousValues must all be assigned BEFORE that call --
|
||||
-- otherwise either a stale callback from whichever addon used the picker last fires, or
|
||||
-- ours fires against state that is not set up yet. Do not reorder this "for tidiness".
|
||||
ColorPickerFrame.func = function()
|
||||
local r, g, b = ColorPickerFrame:GetColorRGB()
|
||||
c.r, c.g, c.b = r, g, b
|
||||
refreshSwatch()
|
||||
end
|
||||
ColorPickerFrame.hasOpacity = nil -- no transparency slider needed
|
||||
ColorPickerFrame.opacityFunc = nil
|
||||
ColorPickerFrame.cancelFunc = function(prevVals)
|
||||
if prevVals then c.r, c.g, c.b = prevVals.r, prevVals.g, prevVals.b end
|
||||
refreshSwatch()
|
||||
end
|
||||
ColorPickerFrame.previousValues = { r = c.r, g = c.g, b = c.b } -- named fields, not an array
|
||||
ColorPickerFrame:SetColorRGB(c.r, c.g, c.b)
|
||||
-- Blizzard's 1.12 colour picker is not draggable out of the box, which is painful when
|
||||
-- it lands on top of what you are trying to recolour. Make it movable once, defensively
|
||||
-- (another addon may already have done it), and leave it that way -- this is a strict
|
||||
-- improvement for every consumer of the frame, not a behaviour change.
|
||||
if not ColorPickerFrame.vfMadeMovable then
|
||||
ColorPickerFrame.vfMadeMovable = true
|
||||
ColorPickerFrame:SetMovable(true) -- already true in 1.12, but be explicit
|
||||
ColorPickerFrame:EnableMouse(true)
|
||||
-- The missing piece: 1.12's picker is movable but registers no drag handler, so
|
||||
-- nothing ever picks it up. Measured in-game: IsMovable() == 1, yet it would not
|
||||
-- budge.
|
||||
ColorPickerFrame:RegisterForDrag("LeftButton")
|
||||
ColorPickerFrame:SetScript("OnDragStart", function() this:StartMoving() end)
|
||||
ColorPickerFrame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
|
||||
end
|
||||
-- Raise it above this window explicitly. Measured in-game: the picker sits at strata
|
||||
-- MEDIUM, so moving our window down to HIGH was not enough -- HIGH still covers MEDIUM.
|
||||
-- A colour picker is modal in spirit; putting it on top is what the user expects, and
|
||||
-- it stays correct no matter what strata this window ends up at later.
|
||||
ColorPickerFrame:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
ShowUIPanel(ColorPickerFrame)
|
||||
end)
|
||||
y = y - 26
|
||||
|
||||
-- ---- SCT position ----------------------------------------------------------------------
|
||||
posLabel = W.MakeText(f, 11, C.ink, "ARTWORK")
|
||||
place(posLabel, -18)
|
||||
posLabel:SetText("SCT position")
|
||||
|
||||
btnMove = W.MakeButton(f, "VampifyOptionsSctMove", "Move", 68, 20)
|
||||
btnMove:SetPoint("TOPLEFT", f, "TOPLEFT", x, y)
|
||||
btnMove:SetScript("OnClick", function()
|
||||
if this.vfDisabled then return end
|
||||
if not (VampifySCT and VampifySCT.setMoveMode and VampifySCT.isMoveMode) then return end
|
||||
local on = not VampifySCT.isMoveMode()
|
||||
VampifySCT.setMoveMode(on)
|
||||
this.text:SetText(on and "Done" or "Move")
|
||||
end)
|
||||
|
||||
-- No X/Y entry fields. They existed briefly and were removed on sight: after a drag the
|
||||
-- stored offsets are raw anchor coordinates (902.75 / -629.99 on a real screen), which
|
||||
-- carry no meaning for anyone positioning a frame by eye. Dragging IS the interface here;
|
||||
-- a number nobody can reason about is worse than no number.
|
||||
|
||||
-- Demo: tuning colour, size and height is guesswork without numbers on screen, and nobody
|
||||
-- wants to pull a mob to check a slider. Fires a burst spread over a few seconds so spacing
|
||||
-- and fade are judgeable, not just the colour.
|
||||
btnDemo = W.MakeButton(f, "VampifyOptionsSctDemo", "Demo", 56, 20)
|
||||
btnDemo:SetPoint("LEFT", btnMove, "RIGHT", 10, 0)
|
||||
btnDemo:SetScript("OnClick", function()
|
||||
if this.vfDisabled then return end
|
||||
if VampifySCT and VampifySCT.demo then VampifySCT.demo(10) end
|
||||
end)
|
||||
|
||||
btnPosReset = W.MakeButton(f, "VampifyOptionsSctPosReset", "Reset", 50, 20)
|
||||
btnPosReset:SetPoint("LEFT", btnDemo, "RIGHT", 10, 0)
|
||||
btnPosReset:SetScript("OnClick", function()
|
||||
if this.vfDisabled then return end
|
||||
local cfg = db()
|
||||
if not (cfg and cfg.sct) then return end
|
||||
local d = VampifyConfig.DEFAULTS.sct.pos
|
||||
cfg.sct.pos = { point = d.point, x = d.x, y = d.y }
|
||||
if VampifySCT and VampifySCT.applyPos then VampifySCT.applyPos() end
|
||||
-- (position fields removed; nothing to mirror)
|
||||
end)
|
||||
y = y - 34
|
||||
|
||||
-- ---- SCT sliders -------------------------------------------------------------------
|
||||
-- Replaces the X/Y entry boxes: raw anchor offsets meant nothing to a human, whereas these
|
||||
-- two are the values you actually want to tune by eye.
|
||||
local function makeSlider(name, label, lo, hi, step, getter, setter)
|
||||
local sl = CreateFrame("Slider", name, f, "OptionsSliderTemplate")
|
||||
sl:SetWidth(200)
|
||||
sl:SetHeight(16)
|
||||
sl:SetMinMaxValues(lo, hi)
|
||||
sl:SetValueStep(step)
|
||||
sl:SetPoint("TOPLEFT", f, "TOPLEFT", x + 6, y)
|
||||
getglobal(name .. "Low"):SetText(tostring(lo))
|
||||
getglobal(name .. "High"):SetText(tostring(hi))
|
||||
local cap = getglobal(name .. "Text")
|
||||
W.ApplyFont(cap, 11)
|
||||
sl.vfLabel = label
|
||||
sl.vfSetter = setter
|
||||
sl.vfGetter = getter
|
||||
sl:SetScript("OnValueChanged", function()
|
||||
local v = this:GetValue()
|
||||
-- OptionsSliderTemplate does not quantise for us in 1.12; round to the step so the
|
||||
-- stored value stays a clean integer instead of 15.9999.
|
||||
v = math.floor(v + 0.5)
|
||||
this.vfSetter(v)
|
||||
getglobal(this:GetName() .. "Text"):SetText(this.vfLabel .. ": " .. v)
|
||||
end)
|
||||
y = y - 42
|
||||
return sl
|
||||
end
|
||||
|
||||
sliderFont = makeSlider("VampifyOptionsFontSlider", "Font size", 8, 48, 1,
|
||||
function()
|
||||
local cfg = db()
|
||||
return (cfg and cfg.sct and cfg.sct.fontSize) or 16
|
||||
end,
|
||||
function(v)
|
||||
local cfg = db()
|
||||
if not (cfg and cfg.sct) then return end
|
||||
cfg.sct.fontSize = v
|
||||
if VampifySCT and VampifySCT.applyFont then VampifySCT.applyFont() end
|
||||
end)
|
||||
|
||||
sliderRise = makeSlider("VampifyOptionsRiseSlider", "SCT height", 20, 320, 5,
|
||||
function()
|
||||
local cfg = db()
|
||||
return (cfg and cfg.sct and cfg.sct.rise) or 40
|
||||
end,
|
||||
function(v)
|
||||
local cfg = db()
|
||||
if not (cfg and cfg.sct) then return end
|
||||
cfg.sct.rise = v
|
||||
end)
|
||||
|
||||
-- Text effect. Cycles rather than three radio buttons: it is one setting with three steps,
|
||||
-- and the label always states the current one, so nothing is hidden behind a menu.
|
||||
local OUTLINES = { "THICKOUTLINE", "OUTLINE", "" }
|
||||
local OUTLINE_LABEL = { THICKOUTLINE = "Thick", OUTLINE = "Thin", [""] = "None" }
|
||||
local function outlineText()
|
||||
local cfg = db()
|
||||
local cur = (cfg and cfg.sct and cfg.sct.outline) or "THICKOUTLINE"
|
||||
return "Outline: " .. (OUTLINE_LABEL[cur] or "Thick")
|
||||
end
|
||||
btnOutline = W.MakeButton(f, "VampifyOptionsOutline", outlineText(), 120, 20)
|
||||
btnOutline:SetPoint("TOPLEFT", f, "TOPLEFT", x + 6, y)
|
||||
btnOutline:SetScript("OnClick", function()
|
||||
if this.vfDisabled then return end
|
||||
local cfg = db()
|
||||
if not (cfg and cfg.sct) then return end
|
||||
local cur = cfg.sct.outline or "THICKOUTLINE"
|
||||
local nxt = OUTLINES[1]
|
||||
for i = 1, table.getn(OUTLINES) do
|
||||
if OUTLINES[i] == cur then nxt = OUTLINES[math.mod(i, table.getn(OUTLINES)) + 1] end
|
||||
end
|
||||
cfg.sct.outline = nxt
|
||||
this.text:SetText(outlineText())
|
||||
if VampifySCT and VampifySCT.applyFont then VampifySCT.applyFont() end
|
||||
if VampifySCT and VampifySCT.demo then VampifySCT.demo(4) end -- show the change at once
|
||||
end)
|
||||
O.refreshOutlineLabel = function()
|
||||
if btnOutline then btnOutline.text:SetText(outlineText()) end
|
||||
end
|
||||
y = y - 28
|
||||
|
||||
-- ---- display size (2026-08-25, feature request: "two more sliders in the options that
|
||||
-- determine the height of the bar and the detail bars") -----------------------------------
|
||||
-- Two sliders, same makeSlider() recipe as the SCT ones above, driving VampifyDisplay's own
|
||||
-- PILL_H/BAR_H (compact-bar capsule) and ROW_H/SPLIT_ROW_H/DETAIL_LINE_H (breakdown-panel
|
||||
-- bars) -- see gui/display.lua's own comments on V.PILL_H_MIN/MAX and
|
||||
-- V.PANEL_BAR_H_MIN/MAX for exactly why those two ranges were chosen (the readable-text
|
||||
-- floor and the "too fat" ceiling it was named after). Bounds are read from VampifyDisplay itself
|
||||
-- rather than copied here a second time, so the two files cannot silently drift apart.
|
||||
-- Setters write the SavedVariables field AND call the matching VampifyDisplay.apply* entry
|
||||
-- point immediately -- same split the SCT sliders above already use (VampifySCT.applyFont/
|
||||
-- applyPos): this file owns persistence, gui/display.lua owns making the change visible with
|
||||
-- no /reload.
|
||||
local sizeLabel = W.MakeText(f, 11, C.ink, "ARTWORK")
|
||||
place(sizeLabel, -18)
|
||||
sizeLabel:SetText("Display size")
|
||||
|
||||
sliderPillH = makeSlider("VampifyOptionsPillHeightSlider", "Bar height",
|
||||
(VampifyDisplay and VampifyDisplay.PILL_H_MIN) or 8,
|
||||
(VampifyDisplay and VampifyDisplay.PILL_H_MAX) or 40, 1,
|
||||
function()
|
||||
local cfg = db()
|
||||
return (cfg and cfg.pillHeight)
|
||||
or (VampifyDisplay and VampifyDisplay.PILL_H_DEFAULT) or 10
|
||||
end,
|
||||
function(v)
|
||||
local cfg = db()
|
||||
if cfg then cfg.pillHeight = v end
|
||||
if VampifyDisplay and VampifyDisplay.applyPillHeight then
|
||||
VampifyDisplay.applyPillHeight(v)
|
||||
end
|
||||
end)
|
||||
W.AttachTooltip(sliderPillH, "Height of the compact bar's capsule (the ST/AoE ratio pill)."
|
||||
.. " The bar itself and its drag-resize grip follow automatically.")
|
||||
|
||||
sliderPanelBarH = makeSlider("VampifyOptionsPanelBarHeightSlider", "Detail bar height",
|
||||
(VampifyDisplay and VampifyDisplay.PANEL_BAR_H_MIN) or 10,
|
||||
(VampifyDisplay and VampifyDisplay.PANEL_BAR_H_MAX) or 40, 1,
|
||||
function()
|
||||
local cfg = db()
|
||||
return (cfg and cfg.panelBarHeight)
|
||||
or (VampifyDisplay and VampifyDisplay.PANEL_BAR_H_DEFAULT) or 18
|
||||
end,
|
||||
function(v)
|
||||
local cfg = db()
|
||||
if cfg then cfg.panelBarHeight = v end
|
||||
if VampifyDisplay and VampifyDisplay.applyPanelBarHeight then
|
||||
VampifyDisplay.applyPanelBarHeight(v)
|
||||
end
|
||||
end)
|
||||
W.AttachTooltip(sliderPanelBarH, "Height of the ability/split bars in the hover breakdown"
|
||||
.. " panel, and (scaled with them) the row-detail panel's own line height.")
|
||||
|
||||
-- divider
|
||||
local divider = f:CreateTexture("VampifyOptionsDivider", "ARTWORK")
|
||||
divider:SetHeight(1)
|
||||
divider:SetWidth(292)
|
||||
divider:SetTexture(C.line.r, C.line.g, C.line.b)
|
||||
divider:SetPoint("TOPLEFT", f, "TOPLEFT", x, y - 4)
|
||||
y = y - 16
|
||||
|
||||
-- ---- buttons ------------------------------------------------------------------------
|
||||
local btnReset = W.MakeButton(f, "VampifyOptionsResetTotals", "Reset totals", 138, 22)
|
||||
btnReset:SetPoint("TOPLEFT", f, "TOPLEFT", x, y)
|
||||
-- Goes through the one reset path rather than repeating it. Reassigning VampifyState here
|
||||
-- would ORPHAN the per-spell tables: since persistence landed they point INTO
|
||||
-- VampifyCharDB, so a fresh state would keep recording to tables nothing saves, silently,
|
||||
-- until the next reload re-wired them. VampifyResetSession clears in place for exactly
|
||||
-- that reason. Scope is "session" -- a button labelled "Reset totals" must not wipe the
|
||||
-- lifetime set, which has to be asked for by name.
|
||||
btnReset:SetScript("OnClick", function()
|
||||
if VampifyResetSession then VampifyResetSession("session") end
|
||||
refreshInfo()
|
||||
end)
|
||||
|
||||
local btnPos = W.MakeButton(f, "VampifyOptionsResetPos", "Reset position", 138, 22)
|
||||
btnPos:SetPoint("TOPLEFT", f, "TOPLEFT", x + 154, y)
|
||||
btnPos:SetScript("OnClick", function() VampifyDisplay.resetPos() end)
|
||||
y = y - 32
|
||||
|
||||
-- divider before the info panel
|
||||
local divider2 = f:CreateTexture("VampifyOptionsDivider2", "ARTWORK")
|
||||
divider2:SetHeight(1)
|
||||
divider2:SetWidth(292)
|
||||
divider2:SetTexture(C.line.r, C.line.g, C.line.b)
|
||||
divider2:SetPoint("TOPLEFT", f, "TOPLEFT", x, y)
|
||||
y = y - 12
|
||||
|
||||
-- ---- info panel -----------------------------------------------------------------------
|
||||
infoFS = W.MakeText(f, 10, C.ink, "ARTWORK")
|
||||
infoFS:SetPoint("TOPLEFT", f, "TOPLEFT", x, y)
|
||||
infoFS:SetWidth(292)
|
||||
infoFS:SetJustifyH("LEFT")
|
||||
infoFS:SetJustifyV("TOP")
|
||||
|
||||
f:SetScript("OnShow", function()
|
||||
refreshChecks()
|
||||
refreshInfo()
|
||||
refreshSwatch()
|
||||
refreshSliders()
|
||||
if O.refreshOutlineLabel then O.refreshOutlineLabel() end
|
||||
refreshEnabled()
|
||||
if btnMove then
|
||||
local on = VampifySCT and VampifySCT.isMoveMode and VampifySCT.isMoveMode()
|
||||
btnMove.text:SetText(on and "Done" or "Move")
|
||||
end
|
||||
elapsed = 0
|
||||
end)
|
||||
|
||||
-- Refreshed only while shown: a hidden frame never receives OnUpdate in 1.12, so this is
|
||||
-- naturally free at rest without an extra IsShown() guard. The swatch rides along on the
|
||||
-- same 0.5s tick. The sliders deliberately do NOT -- they are only pushed on OnShow, since
|
||||
-- writing to a slider fires OnValueChanged and would fight the user mid-drag.
|
||||
f:SetScript("OnUpdate", function()
|
||||
elapsed = elapsed + arg1
|
||||
if elapsed < 0.5 then return end
|
||||
elapsed = 0
|
||||
refreshInfo()
|
||||
-- (position fields removed; nothing to mirror)
|
||||
end)
|
||||
|
||||
table.insert(UISpecialFrames, "VampifyOptionsFrame")
|
||||
|
||||
frame = f
|
||||
f:Hide()
|
||||
return f
|
||||
end
|
||||
|
||||
function O.toggle()
|
||||
local f = build()
|
||||
if f:IsShown() then f:Hide() else f:Show() end
|
||||
end
|
||||
end
|
||||
+431
@@ -0,0 +1,431 @@
|
||||
-- Vampify -- scrolling combat text, our own.
|
||||
--
|
||||
-- Blizzard's FCT (gui/fct.lua) cannot be positioned or fully recolored by an addon --
|
||||
-- Blizzard_CombatText owns its own frame, and its position is not settable from outside. This is
|
||||
-- the alternative engine: a positionable anchor frame, a pooled set of FontStrings, and one shared
|
||||
-- OnUpdate driving every number in flight. Which engine actually runs is chosen by
|
||||
-- VampifyDB.sct.mode ("own" | "bar" | "blizzard") and dispatched from core/commands.lua -- "bar"
|
||||
-- is this SAME engine and pool, just anchored to the display bar instead of a free-floating
|
||||
-- position (see reanchor() below); this file does not know or care that the Blizzard alternative
|
||||
-- exists.
|
||||
--
|
||||
-- Pure-Lua 5.0 dofile-loadable: VampifySCT exists at file scope, but every function that touches
|
||||
-- the WoW API lives inside "if CreateFrame then" -- same shape as gui/display.lua and
|
||||
-- gui/options.lua. resolveAnchorMode below is the one exception: it is pure decision logic (mode +
|
||||
-- bar-frame-exists -> "bar" | "free"), so it lives outside the guard, offline-testable the same
|
||||
-- way gui/display.lua's V.boundFlag is (tools/luatests/test_sct.lua).
|
||||
|
||||
VampifySCT = {}
|
||||
local S = VampifySCT
|
||||
|
||||
-- Which anchor a given mode resolves to. "bar" only when the mode actually asks for it AND the
|
||||
-- bar frame exists yet -- reanchor() below is the reason existence, not visibility, is the gate:
|
||||
-- a hidden-but-created bar can still anchor to, a not-yet-created one cannot. Every other mode,
|
||||
-- including nil (fresh/unmigrated profile) and any future/unknown value, resolves to the
|
||||
-- free-floating anchor rather than erroring -- the safe, always-available fallback.
|
||||
function S.resolveAnchorMode(mode, barExists)
|
||||
if mode == "bar" and barExists then return "bar" end
|
||||
return "free"
|
||||
end
|
||||
|
||||
if CreateFrame then
|
||||
local W = VampifyWidgets
|
||||
local C = W.COLORS
|
||||
|
||||
local POOL_MAX = 20
|
||||
local DUR_FALLBACK = 1.5
|
||||
|
||||
-- Crit pop, in multiples of the configured font size. Ratios taken from Blizzard's own combat
|
||||
-- text: MINHEIGHT 30 and MAXHEIGHT 60 against a base of 25, grown over 0.05s and shrunk back by
|
||||
-- 0.2s (Blizzard_CombatText.lua:4-8).
|
||||
local CRIT_MIN = 30 / 25
|
||||
local CRIT_MAX = 60 / 25
|
||||
local CRIT_GROW = 0.05
|
||||
local CRIT_SHRINK = 0.2
|
||||
|
||||
local anchor, label
|
||||
local moveMode = false
|
||||
local spawnCounter = 0
|
||||
-- Cache of the last anchor mode actually applied ("bar" | "free" | nil before the first
|
||||
-- build). Lets reanchor() below be a cheap no-op on every spawn once nothing has changed,
|
||||
-- instead of an unconditional ClearAllPoints+SetPoint per number -- which matters because it
|
||||
-- would otherwise fight an in-progress free-anchor drag. It cannot: the mode cannot change
|
||||
-- mid-drag (moveMode is only reachable in "own" mode, see S.setMoveMode below), so a drag never
|
||||
-- sees anchorMode flip underneath it.
|
||||
local lastAnchorMode = nil
|
||||
|
||||
-- Fixed-size slot pool: a pre-sized array of records, each either idle or carrying one number.
|
||||
-- Free slots live on a manual LIFO stack (freeStack/freeTop) -- push/pop by hand, never
|
||||
-- table.insert/table.remove/table.getn, so there is no growing "n" field and no need for
|
||||
-- VampifyConst.resetList (core/const.lua): the Lua-5.0 trap that helper guards against simply
|
||||
-- does not exist in this design.
|
||||
local slots = {}
|
||||
local freeStack, freeTop = {}, 0
|
||||
for i = 1, POOL_MAX do
|
||||
slots[i] = { active = false, fs = nil, t = 0, dur = DUR_FALLBACK, dx = 0, dy0 = 0,
|
||||
alphaMul = 1, crit = false, base = 16 }
|
||||
freeTop = freeTop + 1
|
||||
freeStack[freeTop] = i
|
||||
end
|
||||
|
||||
local function db() return VampifyConfig.get() end
|
||||
|
||||
-- Only the border/background/label are hidden in normal operation -- the frame itself always
|
||||
-- stays shown, because the pooled FontStrings are its children and a hidden parent would hide
|
||||
-- them too. "Invisible" here means alpha 0, not SetBackdrop(nil).
|
||||
local function applyMoveVisuals()
|
||||
if moveMode then
|
||||
anchor:EnableMouse(true)
|
||||
anchor:SetBackdropColor(0.08, 0.09, 0.12, 0.85)
|
||||
anchor:SetBackdropBorderColor(C.gold.r, C.gold.g, C.gold.b, 1)
|
||||
if label then label:Show() end
|
||||
else
|
||||
anchor:EnableMouse(false)
|
||||
anchor:SetBackdropColor(0, 0, 0, 0)
|
||||
anchor:SetBackdropBorderColor(0, 0, 0, 0)
|
||||
if label then label:Hide() end
|
||||
end
|
||||
end
|
||||
|
||||
-- Applies the resolved anchor mode to the live frame -- "bar" hangs the anchor off
|
||||
-- VampifyDisplayFrame's top edge (numbers rise FROM the bar's top upward, matching every other
|
||||
-- rising-number layout in this file); anything else restores the stored free position, the
|
||||
-- same positioning logic build() always used. `force` bypasses the lastAnchorMode cache: used
|
||||
-- for the initial build() placement and for an explicit runtime switch (S.applyPos, called from
|
||||
-- gui/options.lua on every mode-radio click and on Reset) so the anchor moves immediately
|
||||
-- instead of waiting for the next spawn. Without force, a spawn's routine call is a no-op
|
||||
-- unless the resolved mode actually changed since last time -- see lastAnchorMode's comment.
|
||||
local function reanchor(force)
|
||||
local cfg = db()
|
||||
local mode = cfg and cfg.sct and cfg.sct.mode
|
||||
local barExists = getglobal("VampifyDisplayFrame") and true or false
|
||||
local anchorMode = S.resolveAnchorMode(mode, barExists)
|
||||
if not force and anchorMode == lastAnchorMode then return end
|
||||
lastAnchorMode = anchorMode
|
||||
anchor:ClearAllPoints()
|
||||
if anchorMode == "bar" then
|
||||
anchor:SetPoint("BOTTOM", VampifyDisplayFrame, "TOP", 0, 6)
|
||||
else
|
||||
local pos = (cfg and cfg.sct and cfg.sct.pos) or { point = "CENTER", x = -180, y = 0 }
|
||||
anchor:SetPoint(pos.point, UIParent, pos.point, pos.x, pos.y)
|
||||
end
|
||||
end
|
||||
|
||||
local function build()
|
||||
if anchor then return anchor end
|
||||
|
||||
local a = CreateFrame("Frame", "VampifySCTAnchor", UIParent)
|
||||
a:SetWidth(140)
|
||||
a:SetHeight(30)
|
||||
-- HIGH, not the default MEDIUM/level 1 -- MEDIUM disappears under pfUI, the exact failure
|
||||
-- gui/display.lua already hit and documented there.
|
||||
a:SetFrameStrata("HIGH")
|
||||
a:SetFrameLevel(10)
|
||||
a:SetMovable(true)
|
||||
a:EnableMouse(false) -- normal operation: out of the mouse's way entirely
|
||||
a:SetClampedToScreen(true)
|
||||
a:RegisterForDrag("LeftButton")
|
||||
a:SetBackdrop(W.SOLID_BACKDROP)
|
||||
|
||||
a:SetScript("OnDragStart", function()
|
||||
if moveMode then this:StartMoving() end
|
||||
end)
|
||||
a:SetScript("OnDragStop", function()
|
||||
this:StopMovingOrSizing()
|
||||
local point, _, _, x, y = this:GetPoint()
|
||||
local c = db()
|
||||
if c and c.sct then c.sct.pos = { point = point, x = x, y = y } end
|
||||
end)
|
||||
|
||||
label = a:CreateFontString("VampifySCTAnchorLabel", "OVERLAY")
|
||||
W.ApplyFont(label, 10)
|
||||
W.SetTextColorG(label, C.goldHi)
|
||||
label:SetPoint("CENTER", a, "CENTER", 0, 0)
|
||||
label:SetText("Vampify SCT")
|
||||
label:Hide()
|
||||
|
||||
anchor = a
|
||||
-- Initial placement: bar anchor if mode == "bar" AND VampifyDisplayFrame already exists at
|
||||
-- this point, else the stored free position. If the bar has not been created yet (load
|
||||
-- order), this falls back to free without error -- spawn()'s own reanchor() call re-checks
|
||||
-- on every emit, lazily, so the very next number picks up the bar once it exists.
|
||||
reanchor(true)
|
||||
anchor:Show() -- always shown; see applyMoveVisuals for why
|
||||
applyMoveVisuals()
|
||||
return a
|
||||
end
|
||||
|
||||
local function fontSize()
|
||||
local cfg = db()
|
||||
local sct = cfg and cfg.sct
|
||||
return (sct and sct.fontSize) or 16
|
||||
end
|
||||
|
||||
-- Outline flag and shadow, the two halves of "readable over anything". Stored as a string
|
||||
-- because that is exactly what SetFont's third argument takes; "" means no outline.
|
||||
local function outlineFlag()
|
||||
local cfg = db()
|
||||
local sct = cfg and cfg.sct
|
||||
local f = sct and sct.outline
|
||||
if f == "" then return nil end
|
||||
return f or "THICKOUTLINE"
|
||||
end
|
||||
|
||||
local function shadowOn()
|
||||
local cfg = db()
|
||||
local sct = cfg and cfg.sct
|
||||
if sct and sct.shadow == false then return false end
|
||||
return true
|
||||
end
|
||||
|
||||
-- How far a number travels over its lifetime. Doubles as the crowding control: a taller rise
|
||||
-- spreads a burst over more vertical space, which is exactly what the user asked this slider
|
||||
-- for -- so the numbers don't overlap so heavily and have more room.
|
||||
local function riseHeight()
|
||||
local cfg = db()
|
||||
local sct = cfg and cfg.sct
|
||||
return (sct and sct.rise) or 40
|
||||
end
|
||||
|
||||
local function ensureFs(i)
|
||||
local s = slots[i]
|
||||
if s.fs then return s.fs end
|
||||
local fs = anchor:CreateFontString("VampifySCTNum"..i, "OVERLAY")
|
||||
W.ApplyFont(fs, fontSize(), outlineFlag())
|
||||
W.ApplyShadow(fs, shadowOn())
|
||||
s.fs = fs
|
||||
return fs
|
||||
end
|
||||
|
||||
-- Re-apply the font to every pooled FontString. Called when the size slider moves: the pool is
|
||||
-- built lazily and lives for the session, so without this only numbers spawned from fresh
|
||||
-- slots would pick up the new size.
|
||||
function S.applyFont()
|
||||
local sz, fl, sh = fontSize(), outlineFlag(), shadowOn()
|
||||
for i = 1, POOL_MAX do
|
||||
if slots[i] and slots[i].fs then
|
||||
W.ApplyFont(slots[i].fs, sz, fl)
|
||||
W.ApplyShadow(slots[i].fs, sh)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Pop a free slot off the LIFO stack -- O(1), no table.remove/table.getn. Only when the stack
|
||||
-- is empty (all 20 numbers in flight at once) does this fall back to an O(n) scan, and even
|
||||
-- then it picks the oldest-active slot: closest to fading out anyway, so recycling it is the
|
||||
-- least noticeable choice.
|
||||
local function acquire()
|
||||
if freeTop > 0 then
|
||||
local i = freeStack[freeTop]
|
||||
freeStack[freeTop] = nil
|
||||
freeTop = freeTop - 1
|
||||
return i
|
||||
end
|
||||
local oldest, oldestT = 1, -1
|
||||
for i = 1, POOL_MAX do
|
||||
if slots[i].t > oldestT then oldestT = slots[i].t; oldest = i end
|
||||
end
|
||||
return oldest
|
||||
end
|
||||
|
||||
-- Push a slot back onto the free stack once its number has fully faded.
|
||||
local function release(i)
|
||||
freeTop = freeTop + 1
|
||||
freeStack[freeTop] = i
|
||||
end
|
||||
|
||||
-- The one shared animation driver for every active number. Attached on the first spawn,
|
||||
-- detached the moment nothing is left in flight -- an idle SCT costs nothing.
|
||||
local function onUpdate()
|
||||
local dt = arg1
|
||||
local any = false
|
||||
for i = 1, POOL_MAX do
|
||||
local s = slots[i]
|
||||
if s.active then
|
||||
s.t = s.t + dt
|
||||
if s.t >= s.dur then
|
||||
s.active = false
|
||||
s.fs:Hide()
|
||||
release(i)
|
||||
else
|
||||
any = true
|
||||
local frac = s.t / s.dur
|
||||
-- Rises sct.rise px over its lifetime; dy0 is an extra starting offset that decays to
|
||||
-- zero as frac -> 1, so a burst of simultaneous numbers starts spread out and
|
||||
-- converges onto the same rising line instead of stacking exactly on top of
|
||||
-- each other for their whole arc. UNVERIFIED: this staggering is not backed by
|
||||
-- any researched pattern (none exists for this in the wow-domain knowledge base
|
||||
-- at time of writing) -- it is a simple, untested heuristic. Watch it in-game.
|
||||
local rise = s.rise
|
||||
if s.crit then
|
||||
-- Blizzard pins a crit at its start position; here it still drifts, but far
|
||||
-- less, so it reads as "popped in place" without freezing on screen.
|
||||
rise = s.rise * 0.25
|
||||
-- Grow, then shrink back -- the pop that distinguishes a crit from a big
|
||||
-- normal number.
|
||||
local h
|
||||
if s.t < CRIT_GROW then
|
||||
h = CRIT_MIN + (CRIT_MAX - CRIT_MIN) * (s.t / CRIT_GROW)
|
||||
elseif s.t < CRIT_SHRINK then
|
||||
h = CRIT_MAX - (CRIT_MAX - CRIT_MIN)
|
||||
* ((s.t - CRIT_GROW) / (CRIT_SHRINK - CRIT_GROW))
|
||||
else
|
||||
h = CRIT_MIN
|
||||
end
|
||||
s.fs:SetTextHeight(s.base * h)
|
||||
end
|
||||
local y = frac * rise + s.dy0 * (1 - frac)
|
||||
s.fs:ClearAllPoints()
|
||||
s.fs:SetPoint("CENTER", anchor, "CENTER", s.dx, y)
|
||||
s.fs:SetAlpha((1 - frac) * s.alphaMul)
|
||||
end
|
||||
end
|
||||
end
|
||||
if not any then anchor:SetScript("OnUpdate", nil) end
|
||||
end
|
||||
|
||||
-- The path that actually spawns a number. S.emit gates this on cfg.sct.enabled; S.demo calls it
|
||||
-- directly so positioning still works while SCT is switched off in the options window.
|
||||
local function spawn(amount, isOverheal, isCrit)
|
||||
build()
|
||||
-- Lazy re-check, not OnUpdate polling: cheap no-op once the resolved anchor mode has not
|
||||
-- changed (see lastAnchorMode), but catches a bar that came into existence after this
|
||||
-- anchor was first built (load-order dependent) on the very next number spawned.
|
||||
reanchor()
|
||||
local cfg = db()
|
||||
local sct = cfg and cfg.sct
|
||||
if not sct then return end
|
||||
|
||||
local i = acquire()
|
||||
local s = slots[i]
|
||||
local fs = ensureFs(i)
|
||||
|
||||
s.active = true
|
||||
s.t = 0
|
||||
s.dur = sct.duration or DUR_FALLBACK
|
||||
s.rise = riseHeight()
|
||||
|
||||
spawnCounter = spawnCounter + 1
|
||||
-- Stagger scales with the rise: a taller column gets proportionally wider lanes and bigger
|
||||
-- starting offsets, so raising the slider genuinely buys room instead of just stretching
|
||||
-- the same crowding over a longer arc.
|
||||
local spread = s.rise / 40
|
||||
if math.mod(spawnCounter, 2) == 0 then s.dx = 12 * spread else s.dx = -12 * spread end
|
||||
s.dy0 = math.mod(spawnCounter, 3) * 6 * spread
|
||||
if isOverheal then s.alphaMul = 0.5 else s.alphaMul = 1 end
|
||||
|
||||
-- Vampirism itself never crits. A return CAUSED by a crit is marked the way the client's
|
||||
-- own combat text marks one -- and that is not merely "bigger": Blizzard grows the number,
|
||||
-- shrinks it back, and keeps it in place instead of scrolling it away
|
||||
-- (Blizzard_CombatText.lua:280-282, 336-339). Copying the behaviour, not just the size, is
|
||||
-- what makes it read as a crit at a glance.
|
||||
s.crit = isCrit and true or false
|
||||
s.base = fontSize()
|
||||
W.ApplyFont(fs, s.base, outlineFlag())
|
||||
W.ApplyShadow(fs, shadowOn())
|
||||
if s.crit then
|
||||
-- SetTextHeight scales the rendered text without touching the font object, which is why
|
||||
-- Blizzard animates with it rather than re-applying a font every frame.
|
||||
fs:SetTextHeight(s.base * CRIT_MIN)
|
||||
else
|
||||
fs:SetTextHeight(s.base)
|
||||
end
|
||||
|
||||
local col = sct.color or { r = 0.4, g = 0.9, b = 0.4 }
|
||||
fs:SetTextColor(col.r, col.g, col.b)
|
||||
-- Overheal is parenthesized rather than minus-prefixed: it is still healing, and a leading
|
||||
-- "-" would read as damage at a glance. The halved alpha above is the other half of telling
|
||||
-- it apart from a real crossing.
|
||||
if isOverheal then
|
||||
fs:SetText("("..amount..")")
|
||||
else
|
||||
fs:SetText("+"..amount)
|
||||
end
|
||||
fs:ClearAllPoints()
|
||||
fs:SetPoint("CENTER", anchor, "CENTER", s.dx, 0)
|
||||
fs:SetAlpha(1 * s.alphaMul)
|
||||
fs:Show()
|
||||
|
||||
anchor:SetScript("OnUpdate", onUpdate)
|
||||
end
|
||||
|
||||
-- amount is the integer to show -- an accumulator crossing for real heals (core/model.lua),
|
||||
-- a per-hit floored amount for overheal (core/commands.lua) -- never a raw float. Zero
|
||||
-- crossings are not emitted: the accumulator does not cross an integer on every hit, and a
|
||||
-- stream of "+0" would just be noise (the same rule gui/fct.lua enforced before it).
|
||||
function S.emit(amount, isOverheal, isCrit)
|
||||
if not amount or amount <= 0 then return end
|
||||
local cfg = db()
|
||||
if not cfg or not cfg.sct or not cfg.sct.enabled then return end
|
||||
spawn(amount, isOverheal, isCrit)
|
||||
end
|
||||
|
||||
-- Three sample numbers so the user can see where they land while dragging the anchor. Bypasses
|
||||
-- the enabled gate on purpose: positioning must work even with SCT switched off.
|
||||
-- A burst spread over a couple of seconds, not three numbers at once: the whole point of the
|
||||
-- demo is to judge spacing, rise and fade while tuning the sliders, and all three of those are
|
||||
-- only visible when numbers arrive the way they do in a fight. Runs on its own ticker frame so
|
||||
-- it cannot interfere with the animation driver, and detaches itself when finished.
|
||||
local demoLeft, demoAcc, demoTicker = 0, 0, nil
|
||||
|
||||
function S.demo(count)
|
||||
build()
|
||||
demoLeft = count or 10
|
||||
demoAcc = 0
|
||||
if not demoTicker then
|
||||
demoTicker = CreateFrame("Frame", "VampifySCTDemoFrame", UIParent)
|
||||
end
|
||||
demoTicker:SetScript("OnUpdate", function()
|
||||
demoAcc = demoAcc + arg1
|
||||
if demoAcc < 0.25 then return end
|
||||
demoAcc = 0
|
||||
if demoLeft <= 0 then
|
||||
this:SetScript("OnUpdate", nil)
|
||||
return
|
||||
end
|
||||
demoLeft = demoLeft - 1
|
||||
-- Every style the real thing can produce, because a demo that only shows one of them
|
||||
-- lets you tune half the appearance: plain returns, dimmed overheal in parentheses, and
|
||||
-- crit-caused returns in the larger font. Crits carry a bigger number, since a crit is
|
||||
-- a bigger hit and therefore a bigger return.
|
||||
local phase = math.mod(demoLeft, 5)
|
||||
local isOver = (phase == 0)
|
||||
local isCrit = (phase == 2 or phase == 3)
|
||||
local n = math.mod(demoLeft, 4) + 3
|
||||
if isCrit then n = n * 2 end
|
||||
spawn(n, isOver, isCrit)
|
||||
end)
|
||||
end
|
||||
|
||||
-- Returns true on success. Refuses to turn ON while anchored to the bar (mode == "bar") --
|
||||
-- dragging is meaningless there, the anchor point is derived from VampifyDisplayFrame, not a
|
||||
-- stored position -- and returns false plus a one-line reason instead, so callers (currently
|
||||
-- core/commands.lua's "/vf sctmove") can tell the player why nothing happened. Turning OFF is
|
||||
-- always allowed; there is nothing to refuse there.
|
||||
function S.setMoveMode(on)
|
||||
build()
|
||||
if on then
|
||||
local cfg = db()
|
||||
local mode = cfg and cfg.sct and cfg.sct.mode
|
||||
if mode == "bar" then
|
||||
return false, "SCT is anchored to the bar in this mode -- switch to \"Free anchor\""
|
||||
.." in Options to move it."
|
||||
end
|
||||
end
|
||||
moveMode = on and true or false
|
||||
applyMoveVisuals()
|
||||
if moveMode then S.demo() end
|
||||
return true
|
||||
end
|
||||
|
||||
function S.isMoveMode()
|
||||
return moveMode
|
||||
end
|
||||
|
||||
-- Explicit, forced re-anchor: called from gui/options.lua on every engine-mode radio click and
|
||||
-- on "Reset" -- both want the anchor to move at once, not wait for the next spawn's lazy check.
|
||||
function S.applyPos()
|
||||
build()
|
||||
reanchor(true)
|
||||
end
|
||||
end
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
-- Vampify -- shared UI building blocks, ported from DopingControl's ui/widgets.lua.
|
||||
--
|
||||
-- Slim port: only what this addon's two windows actually use -- panel backdrop, a solid 1px
|
||||
-- backdrop, a pfUI-aware font helper, a text factory, a dark gold-trim button, the change-guarded
|
||||
-- setters that keep an idle window at zero allocation, and a minimal tooltip attach helper. The
|
||||
-- cell/pill/badge factories and the two-column tooltip helper in the source addon exist for its
|
||||
-- raid-roster grid, which this addon does not have, so they are not ported.
|
||||
--
|
||||
-- Pure Lua 5.0, dofile-loadable: top level only defines tables/functions; CreateFrame and friends
|
||||
-- are only referenced INSIDE factory functions, which are never called offline.
|
||||
|
||||
VampifyWidgets = {}
|
||||
local W = VampifyWidgets
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Texture / font constants
|
||||
-- ------------------------------------------------------------------
|
||||
W.SOLID_TEX = "Interface\\Buttons\\WHITE8X8"
|
||||
W.FONT = "Fonts\\FRIZQT__.TTF"
|
||||
|
||||
-- Main window backdrop. Textures shipped in Vampify\textures\ -- copied from DopingControl's own
|
||||
-- panel_bg/panel_border, NOT referenced through DopingControl's AddOns path: this addon must not
|
||||
-- depend on DopingControl being installed.
|
||||
W.PANEL_BACKDROP = {
|
||||
bgFile = "Interface\\AddOns\\Vampify\\textures\\panel_bg",
|
||||
edgeFile = "Interface\\AddOns\\Vampify\\textures\\panel_border",
|
||||
tile = true, tileSize = 128, edgeSize = 16,
|
||||
insets = { left = 5, right = 5, top = 5, bottom = 5 },
|
||||
}
|
||||
|
||||
-- Solid 1px-border backdrop (WHITE8X8 recipe): bg + border colored via SetBackdropColor /
|
||||
-- SetBackdropBorderColor on the frame that uses it.
|
||||
W.SOLID_BACKDROP = {
|
||||
bgFile = W.SOLID_TEX,
|
||||
edgeFile = W.SOLID_TEX,
|
||||
tile = false, edgeSize = 1,
|
||||
insets = { left = 1, right = 1, top = 1, bottom = 1 },
|
||||
}
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Color palette. Only the entries this addon's two windows use: the gold family for the title and
|
||||
-- button text, the panel/line neutrals for button states, and green/red for the two warning lines
|
||||
-- that need to say something is wrong.
|
||||
-- ------------------------------------------------------------------
|
||||
local function hexrgb(hex)
|
||||
return {
|
||||
r = tonumber(string.sub(hex, 1, 2), 16) / 255,
|
||||
g = tonumber(string.sub(hex, 3, 4), 16) / 255,
|
||||
b = tonumber(string.sub(hex, 5, 6), 16) / 255,
|
||||
}
|
||||
end
|
||||
|
||||
W.COLORS = {
|
||||
goldHi = hexrgb("e6c87e"), -- title / highlight gold
|
||||
gold = hexrgb("8f7440"), -- hover border gold
|
||||
goldSoft = hexrgb("54462a"), -- resting border gold
|
||||
panel2 = hexrgb("171b23"), -- resting button bg
|
||||
panel3 = hexrgb("1d222c"), -- hover button bg
|
||||
line = hexrgb("262c37"), -- standard borders / dividers
|
||||
ink = hexrgb("c9cdd4"), -- body text
|
||||
dim = hexrgb("868d99"), -- secondary text
|
||||
faint = hexrgb("5b6270"), -- tertiary / hints
|
||||
warn = hexrgb("e0c98a"), -- accuracy-hint yellow
|
||||
bad = hexrgb("d05252"), -- no-data red
|
||||
-- Added for the compact-bar/breakdown-panel mockup redesign (2026-08-24): Single-Target vs.
|
||||
-- AoE get their own hue everywhere the two are told apart (the bar's badge group, the panel's
|
||||
-- split row, each ability row's two-color fill), HPS gets its own accent on the bar, badgeBg
|
||||
-- backs the bar's badge-group pill so it reads as its own grouped control rather than more
|
||||
-- bar text.
|
||||
stBlue = hexrgb("5b8fd9"), -- Single-Target bare color chips (the compact-bar pill segment)
|
||||
aoeOrange = hexrgb("d98a3d"), -- AoE bare color chips (the compact-bar pill segment)
|
||||
hpsGreen = hexrgb("7cc576"), -- compact-bar HPS segment
|
||||
badgeBg = hexrgb("11151c"), -- compact-bar badge-group background (legacy, panel row bg reuse)
|
||||
|
||||
-- CANONICAL red/blue pair (in-game correction, reported verbatim: "same color coding as in the
|
||||
-- bar (red/blue)") -- unify the breakdown panel onto the SAME two colors the compact bar's
|
||||
-- own pill already uses, from this ONE shared source, not defined twice). RED = Vamp healing
|
||||
-- landed on the player's CURRENT TARGET, BLUE = everything else -- the same semantics
|
||||
-- everywhere these are used (the compact pill, the panel's split row, each ability row's
|
||||
-- two-color fill). Values verbatim from what used to be gui/display.lua's own local PILL_RED/
|
||||
-- PILL_BLUE (196/255,62/255,58/255 and 53/255,96/255,161/255) -- moved here so both call sites
|
||||
-- read the identical table and can never drift apart again. Also passes this file's own 4.5:1
|
||||
-- bar-text floor with real headroom (verified via an offline contrast-check render script:
|
||||
-- ink-on-red 10.88:1, ink-on-blue 11.91:1 -- stronger than the retired stBlueFill/aoeOrangeFill
|
||||
-- pair's own 6.01:1/5.15:1), so no separate "under text" darkened variant is needed the way the
|
||||
-- old blue/orange pair required.
|
||||
targetRed = hexrgb("C43E3A"),
|
||||
restBlue = hexrgb("3560A1"),
|
||||
-- Neutral grey bar fill for the breakdown panel's collapsed "Other (N)" row (Befund 4) -- a
|
||||
-- deliberately different hue from targetRed/restBlue/goldSoft so a mixed ST+AoE summary
|
||||
-- row never reads as a real, classified ability. White-on-otherGrey measures 7.96:1.
|
||||
otherGrey = hexrgb("4a5160"),
|
||||
-- Near-white text drawn directly on top of a bar fill (split row, ability rows) -- paired with
|
||||
-- the OUTLINE font flag (W.ApplyFont's third argument) at those call sites for the "1px dark
|
||||
-- outline" kept in addition to the contrast fix itself.
|
||||
barText = { r = 0.97, g = 0.96, b = 0.94 },
|
||||
}
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Font helper. Keeps OUR pixel size -- this addon's layout is hand-fitted to it -- and only takes
|
||||
-- pfUI's font FAMILY when pfUI is present, so text reads consistently with the rest of the client
|
||||
-- without pfUI's global size setting reflowing this window.
|
||||
-- ------------------------------------------------------------------
|
||||
-- `flags` is SetFont's third argument: nil, "OUTLINE" or "THICKOUTLINE" (also "MONOCHROME", which
|
||||
-- we do not use). An outline is what makes light text survive a light background -- green numbers
|
||||
-- over grass being the case that prompted it.
|
||||
function W.ApplyFont(fs, size, flags)
|
||||
size = size or 10
|
||||
if pfUI and pfUI.font_default then
|
||||
fs:SetFont(pfUI.font_default, size, flags)
|
||||
else
|
||||
fs:SetFont(W.FONT, size, flags)
|
||||
end
|
||||
end
|
||||
|
||||
-- A soft drop shadow on a whole BACKDROP frame (the compact bar, the breakdown panel) -- pfUI's
|
||||
-- own pfUI.api.CreateBackdropShadow(f), confirmed live-used at pfUI/modules/gui.lua:495-family
|
||||
-- call sites and real in this client (not a Retail-only API). No-op when pfUI is not loaded --
|
||||
-- this is pure cosmetic polish for the mockup redesign (2026-08-24), never a load-bearing part of
|
||||
-- the layout, so a client without pfUI must render exactly as before (flat 1px SOLID_BACKDROP
|
||||
-- border, no shadow) rather than error or leave a gap where the shadow would have been.
|
||||
function W.ApplyBackdropShadow(f)
|
||||
if pfUI and pfUI.api and pfUI.api.CreateBackdropShadow then
|
||||
pfUI.api.CreateBackdropShadow(f)
|
||||
end
|
||||
end
|
||||
|
||||
-- A drop shadow on top of the outline. The two solve different halves of the same problem: the
|
||||
-- outline separates the glyph from the background, the shadow gives it depth against a background
|
||||
-- of the SAME brightness, where an outline of the wrong colour would disappear too.
|
||||
function W.ApplyShadow(fs, on)
|
||||
if on then
|
||||
fs:SetShadowColor(0, 0, 0, 0.9)
|
||||
fs:SetShadowOffset(1, -1)
|
||||
else
|
||||
fs:SetShadowOffset(0, 0)
|
||||
fs:SetShadowColor(0, 0, 0, 0)
|
||||
end
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Change-guarded setters. Convention: cache the last-applied value on the region as vfLast*; only
|
||||
-- call the real setter when the value changed. This is what lets the options window's throttled
|
||||
-- info panel repaint every 0.5s without a SetText call (and the redraw it costs) on every tick.
|
||||
-- ------------------------------------------------------------------
|
||||
function W.SetTextG(fs, text)
|
||||
if fs.vfLastText ~= text then
|
||||
fs.vfLastText = text
|
||||
fs:SetText(text)
|
||||
end
|
||||
end
|
||||
|
||||
function W.SetTextColorG(fs, c)
|
||||
if fs.vfLastColor ~= c then
|
||||
fs.vfLastColor = c
|
||||
fs:SetTextColor(c.r, c.g, c.b)
|
||||
end
|
||||
end
|
||||
|
||||
function W.SetBackdropG(f, bg, border)
|
||||
if f.vfLastBg ~= bg then
|
||||
f.vfLastBg = bg
|
||||
f:SetBackdropColor(bg.r, bg.g, bg.b, bg.a or 1)
|
||||
end
|
||||
if f.vfLastBorder ~= border then
|
||||
f.vfLastBorder = border
|
||||
f:SetBackdropBorderColor(border.r, border.g, border.b, 1)
|
||||
end
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- FontString factory.
|
||||
-- ------------------------------------------------------------------
|
||||
function W.MakeText(parent, size, color, layer)
|
||||
local fs = parent:CreateFontString(nil, layer or "OVERLAY")
|
||||
W.ApplyFont(fs, size)
|
||||
if color then
|
||||
W.SetTextColorG(fs, color)
|
||||
end
|
||||
return fs
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Dark gold-trim button, the addon's one button style.
|
||||
-- ------------------------------------------------------------------
|
||||
function W.MakeButton(parent, name, label, width, height)
|
||||
local C = W.COLORS
|
||||
local b = CreateFrame("Button", name, parent)
|
||||
b:SetWidth(width or 100)
|
||||
b:SetHeight(height or 22)
|
||||
b:SetBackdrop(W.SOLID_BACKDROP)
|
||||
W.SetBackdropG(b, C.panel2, C.goldSoft)
|
||||
b.text = b:CreateFontString(nil, "OVERLAY")
|
||||
W.ApplyFont(b.text, 10)
|
||||
b.text:SetPoint("CENTER", b, "CENTER", 0, 0)
|
||||
W.SetTextColorG(b.text, C.goldHi)
|
||||
b.text:SetText(label)
|
||||
b:SetScript("OnEnter", function()
|
||||
if this.vfDisabled then return end
|
||||
W.SetBackdropG(this, C.panel3, C.gold)
|
||||
end)
|
||||
b:SetScript("OnLeave", function()
|
||||
if this.vfDisabled then return end
|
||||
W.SetBackdropG(this, C.panel2, C.goldSoft)
|
||||
end)
|
||||
return b
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Enable/disable for MakeButton buttons. A hand-drawn button (SOLID_BACKDROP + its own text, no
|
||||
-- Blizzard disabled-texture) needs its dimming done by hand: native Button:Disable() stops OnClick
|
||||
-- from firing, but the resting/hover backdrop colors and the gold text would otherwise still read
|
||||
-- as "clickable". b.vfDisabled additionally guards MakeButton's own OnEnter/OnLeave above so a
|
||||
-- disabled button does not light up on hover.
|
||||
-- ------------------------------------------------------------------
|
||||
-- Sliders in 1.12 have NO Enable()/Disable() -- those are Button methods, and calling them on a
|
||||
-- Slider throws "attempt to call method `Enable' (a nil value)" (caught in-game 2026-08-10).
|
||||
-- Blizzard's own way, from FrameXML/OptionsFrame.lua:481-493, is to hide the thumb and grey the
|
||||
-- three label font strings; EnableMouse(false) is added here so the track ignores clicks too.
|
||||
function W.SetSliderEnabled(sl, enabled)
|
||||
if not sl then return end
|
||||
local C = W.COLORS
|
||||
local name = sl:GetName()
|
||||
local thumb = getglobal(name .. "Thumb")
|
||||
local txt, lo, hi = getglobal(name .. "Text"), getglobal(name .. "Low"), getglobal(name .. "High")
|
||||
local col = enabled and C.ink or C.faint
|
||||
sl:EnableMouse(enabled and true or false)
|
||||
if thumb then if enabled then thumb:Show() else thumb:Hide() end end
|
||||
if txt then txt:SetTextColor(col.r, col.g, col.b) end
|
||||
if lo then lo:SetTextColor(col.r, col.g, col.b) end
|
||||
if hi then hi:SetTextColor(col.r, col.g, col.b) end
|
||||
end
|
||||
|
||||
function W.SetButtonEnabled(b, enabled)
|
||||
local C = W.COLORS
|
||||
if enabled then
|
||||
b.vfDisabled = nil
|
||||
b:Enable()
|
||||
W.SetBackdropG(b, C.panel2, C.goldSoft)
|
||||
W.SetTextColorG(b.text, C.goldHi)
|
||||
else
|
||||
b.vfDisabled = true
|
||||
b:Disable()
|
||||
W.SetBackdropG(b, C.panel2, C.line)
|
||||
W.SetTextColorG(b.text, C.faint)
|
||||
end
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Minimal tooltip attach: chains any existing OnEnter/OnLeave (no HookScript on 1.12), shows a
|
||||
-- single-line tooltip. The convention matches DopingControl's AddTooltip/TipLine pair, trimmed to
|
||||
-- the one line shape this addon needs.
|
||||
-- ------------------------------------------------------------------
|
||||
function W.TipLine(text, c)
|
||||
if not GameTooltip then return end
|
||||
local r, g, b = 1, 1, 1
|
||||
if c then r, g, b = c.r, c.g, c.b end
|
||||
GameTooltip:AddLine(text, r, g, b, 1)
|
||||
end
|
||||
|
||||
function W.AttachTooltip(frame, text)
|
||||
local oldEnter = frame:GetScript("OnEnter")
|
||||
local oldLeave = frame:GetScript("OnLeave")
|
||||
frame.vfTip = text
|
||||
frame:SetScript("OnEnter", function()
|
||||
if oldEnter then oldEnter() end
|
||||
if GameTooltip and this.vfTip then
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText(this.vfTip, 1, 1, 1, 1, 1)
|
||||
GameTooltip:Show()
|
||||
end
|
||||
end)
|
||||
frame:SetScript("OnLeave", function()
|
||||
if oldLeave then oldLeave() end
|
||||
if GameTooltip then GameTooltip:Hide() end
|
||||
end)
|
||||
end
|
||||
Reference in New Issue
Block a user