ad50ebe636
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.
807 lines
42 KiB
Lua
807 lines
42 KiB
Lua
-- 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
|