diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 9b712fd..0000000 --- a/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Editor / OS noise -.DS_Store -Thumbs.db -*.log diff --git a/CHANGELOG.md b/CHANGELOG.md index 45b0ae0..789ce78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,26 +2,20 @@ All notable changes to Vampify are documented here. -## Unreleased +## 0.4.0 — 2026-08-30 ### Added -- MIT license, this changelog, and a README written for players rather than for the author. - -### Fixed -- Long lines in a row's detail panel are wrapped instead of being drawn over the lines below them. -- Tooltips raised while the breakdown panel is open now appear above it rather than behind it. -- The whole bar can be dragged when it is unlocked. Several parts of it — the leftmost number, the - ratio pill, the three small buttons — used to swallow the drag and act as dead spots. The resize - grip still resizes. -- Bar fills, their shading and their gloss are each drawn on their own layer. They previously shared - one, where this client leaves the order between them undefined and re-decides it on every reload, - so a bar could render flat, or in one reported case not at all. +- Options window controls for two settings that were already live but had no UI: a "Fade duration" + slider (0.5-4.0s) and a "Text shadow" checkbox for the scrolling combat text, next to the outline + cycle button. Both are also reachable from the command line: `/vf sct duration ` and + `/vf sct shadow on|off`. +- The glass-bar textures the 0.3.0 upload left out (`textures/hps_orb_*` and + `textures/pill_*_cap`). Without them the bars drew without their fill, shading and gloss. ### Changed -- The display bar is compact: the prefix and the session ratio are gone, and it shows the equipped - loadout percentage together with the number of sources it found. -- The per-hit export records maximum health per hit, so an offline analysis can tell overheal apart - from healing that never arrived. +- The per-hit export carries three more fields for the offline analysis: each source's own + percentage, the zone, and whether the hit followed a cast of the same spell (item and enchant + procs never do). All three are optional, so an older reader still parses the line. ## 0.3.0 — 2026-08-22 @@ -34,12 +28,17 @@ All notable changes to Vampify are documented here. - The damage-over-time channel is measured rather than estimated; the display no longer hedges it. - AoE damping is applied only where a hit is proven to have struck two or more distinct targets, rather than inferred from the spell. +- The display bar is compact: the prefix and the session ratio are gone, and it shows the equipped + loadout percentage together with the number of sources it found. +- The per-hit export records maximum health per hit, so an offline analysis can tell overheal apart + from healing that never arrived. ### Added - Per-hit debug export for measuring the per-source factor, with a persisted toggle and an options checkbox. - Spell exclusions: a spell can be stopped from counting as a Vampirism trigger, and the exclusion survives a reload. +- MIT license, this changelog, and a README written for players rather than for the author. ### Fixed - The raw per-hit capture no longer depends on the watch-mode group guard. @@ -48,6 +47,14 @@ All notable changes to Vampify are documented here. can actually trust, instead of firing on a quiet moment. - An AoE correction removes the integers it actually credited rather than reconstructing them. - A mid-fight gear swap is only checked for while a fight is running. +- Long lines in a row's detail panel are wrapped instead of being drawn over the lines below them. +- Tooltips raised while the breakdown panel is open now appear above it rather than behind it. +- The whole bar can be dragged when it is unlocked. Several parts of it — the leftmost number, the + ratio pill, the three small buttons — used to swallow the drag and act as dead spots. The resize + grip still resizes. +- Bar fills, their shading and their gloss are each drawn on their own layer. They previously shared + one, where this client leaves the order between them undefined and re-decides it on every reload, + so a bar could render flat, or in one reported case not at all. ## 0.2.0 — 2026-08-10 diff --git a/Vampify.toc b/Vampify.toc index 67159b8..b0ff1da 100644 --- a/Vampify.toc +++ b/Vampify.toc @@ -2,7 +2,7 @@ ## Title: Vampify ## Notes: Tracks the healing returned by the Vampirism item stat. ## Author: ShempError -## Version: 0.3.0 +## Version: 0.4.0 ## SavedVariables: VampifyDB ## SavedVariablesPerCharacter: VampifyCharDB diff --git a/capture/damage.lua b/capture/damage.lua index 9372a5b..88e6eee 100644 --- a/capture/damage.lua +++ b/capture/damage.lua @@ -117,6 +117,42 @@ function G.onDamage(fn) table.insert(listeners, fn) end local goSeen = false local damageSeen = false +-- ---- "did the PLAYER cast this, or did it proc?" (per-hit export field `cast`) ---------------- +-- +-- The same SPELL_GO_SELF that latches goSeen above is also the only observable in the client that +-- separates a CAST spell from a PROCCED one: it precedes the damage events of a cast and never +-- fires for an item/enchant proc (the sentence right above G.deriveAoE says exactly this, and it +-- is why deriveAoE had to exist at all). Hypothesis H2 -- that Vampirism is gated on the TRIGGER +-- PATH ("was this triggered by an aura") rather than on "is this an item proc" -- turns on that +-- distinction for one and the same spell id, so the offline analysis needs it recorded per hit +-- rather than inferred from timing (see core/perhit.lua's formatHitLine comment). +-- +-- Keyed by spell id, and deliberately WITHOUT the eviction sweep this project requires of +-- per-GUID tables: the key space here is the player's own castable spell ids, which is bounded by +-- the spellbook and does not grow with time or with mob spawns. That is the exact property the +-- eviction rule exists to protect against, and it does not apply. +local lastGo = {} + +-- Pure, so the predicate is testable without firing events: "was there a cast of this spell id +-- within `window` seconds before `now`". Default window 1.5s -- comfortably longer than the gap +-- between SPELL_GO_SELF and the damage events of the same cast (the GO precedes them within the +-- same or the next frame), and short enough that the PREVIOUS cast of the same spell (global +-- cooldown 1.5s at the very fastest) cannot be mistaken for this one. +local CAST_WINDOW = 1.5 +G.CAST_WINDOW = CAST_WINDOW + +function G.noteCast(spellId, t) + if spellId then lastGo[spellId] = t end +end + +function G.castSeenFor(spellId, now, window) + if not spellId then return false end + local t = lastGo[spellId] + if not t or not now then return false end + local dt = now - t + return dt >= 0 and dt <= (window or CAST_WINDOW) +end + -- ---- AoE derivation from distinct targets, cast or not (spec 4.2, extended 2026-08-22) -------- -- -- Originally written as the fallback for weapon/item procs (Force Reactive Disc's damage shield @@ -248,6 +284,10 @@ if CreateFrame then -- actual decision for every hit of this cast still runs through G.deriveAoE below, the -- exact same distinct-target proof a no-cast proc already has to provide. goSeen = true + -- arg2 is the cast's spell id (see the event's field list at the top of this file). + -- Noting it is what lets a later damage event of the SAME id say "this one was cast", + -- which is the `cast` field of the per-hit export -- see G.castSeenFor above. + G.noteCast(arg2, GetTime()) elseif event == "SPELL_DAMAGE_EVENT_SELF" then local amount, tgt, spellId, caster = G.decodeSpell(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) -- Trust arg2 rather than the _SELF suffix, and never count a hit on ourselves. diff --git a/core/commands.lua b/core/commands.lua index 30f89e2..caa7f89 100644 --- a/core/commands.lua +++ b/core/commands.lua @@ -319,6 +319,28 @@ local lastHp, lastAt, lastExpected = nil, nil, nil local nextCheck = 0 local WATCH_INTERVAL = 15 -- slow on purpose: findings are read by a human, not a loop +-- ---- the zone, for the per-hit export --------------------------------------------------------- +-- +-- Cached rather than read per hit. GetRealZoneText() is cheap but it RETURNS A STRING, and a +-- string per damage event is exactly the per-frame allocation this project forbids in hot paths +-- (30-60 hits/sec during an AoE pull). The zone changes on a loading screen and nowhere else, so +-- it is refreshed from the two events that already fire there. +-- +-- WHY IT IS RECORDED AT ALL: an offline analysis could not answer "use only the Shadowfang Keep +-- data" -- the export had no way to tell one grinding session's zone from another's, and mob +-- level (and therefore hit size, and therefore how often the per-source floor of 1 dominates the +-- percentage) varies enormously between them. See docs/VAMPIRISM-MODELL-STAND §4. +-- +-- Pipes are stripped because "|" is this format's field separator; no real zone name contains one, +-- but a parser that splits on "|" must not be able to be broken by a name at all. +local zoneName = "?" +local function refreshZone() + local z = GetRealZoneText and GetRealZoneText() + if not z or z == "" then z = GetZoneText and GetZoneText() end + if not z or z == "" then zoneName = "?" return end + zoneName = string.gsub(z, "|", "") +end + -- print_ already prefixes "Vampify", so this must not repeat it -- the first field build did, and -- the result read "Vampify Vampify watch:". -- @@ -485,6 +507,13 @@ if VampifyDamage.onDamage then pred_heal = healFloat, acc_total = VampifyState.acc.total, crit = isCrit, + -- The three fields the offline analysis was missing (docs/VAMPIRISM-MODELL-STAND + -- §4). pcts hands over the LIVE list -- PH.recordHit serialises it immediately + -- rather than holding the reference, which matters because this very table is + -- refilled in place on every gear change (see recompute above). + pcts = sourcePercents, + zone = zoneName, + cast = VampifyDamage.castSeenFor and VampifyDamage.castSeenFor(spellId, GetTime()), }, UnitHealth and UnitHealth("player"), GetTime(), UnitHealthMax and UnitHealthMax("player")) end @@ -823,6 +852,9 @@ if CreateFrame then f:RegisterEvent("PLAYER_CAMPING") f:RegisterEvent("PLAYER_QUITING") f:RegisterEvent("LOGOUT_CANCEL") + -- Fires on every loading screen and on every zone border crossed on foot -- the only two ways + -- the cached zone name above can go stale. + f:RegisterEvent("ZONE_CHANGED_NEW_AREA") -- The running count of combat seconds the "no damage events at all" verdict is given from. -- Declared out here rather than beside the OnUpdate that uses it because the combat-end branch @@ -860,8 +892,13 @@ if CreateFrame then end return end + if event == "ZONE_CHANGED_NEW_AREA" then + refreshZone() + return + end if event == "PLAYER_LOGIN" or event == "PLAYER_ENTERING_WORLD" then if event == "PLAYER_ENTERING_WORLD" then wireSession() end + refreshZone() recompute() syncPerHit() if VampifyMinimap and VampifyMinimap.update then VampifyMinimap.update() end @@ -1090,6 +1127,8 @@ local function help() print_("/vf pos -- move the display back to its default position") print_("/vf lock -- toggle frame dragging") print_("/vf fct / sct -- toggle scrolling combat text (either engine)") + print_("/vf sct duration -- SCT fade duration, 0.5-4.0 seconds") + print_("/vf sct shadow on|off -- SCT text shadow") print_("/vf sctmove -- move our own SCT anchor (options window has the same button)") print_("/vf options -- open the options window") print_("/vf source add -- add a manual source, if tooltip detection fails") @@ -1163,12 +1202,39 @@ SlashCmdList["VAMPIFY"] = function(msg) elseif cmd == "fct" or cmd == "sct" then local cfg = VampifyConfig.get() if not cfg or not cfg.sct then print_("config not loaded yet.") return end - cfg.sct.enabled = not cfg.sct.enabled - if cfg.sct.enabled and cfg.sct.mode == "blizzard" and SHOW_COMBAT_TEXT ~= "1" then - print_("scrolling combat text ON -- but Blizzard's own combat text is disabled in its" - .." options, so nothing will show in blizzard mode.") + -- Bare "/vf sct" / "/vf fct" keeps its original meaning (toggle enabled); "sct " + -- is the same sub/val dispatch pattern /vf source and /vf exclude already use above. %S* + -- rather than %d* for val: duration needs a decimal ("1.5"), shadow needs a word (on/off). + local _, _, sub, val = string.find(rest, "^(%a*)%s*(%S*)") + if sub == "" then + cfg.sct.enabled = not cfg.sct.enabled + if cfg.sct.enabled and cfg.sct.mode == "blizzard" and SHOW_COMBAT_TEXT ~= "1" then + print_("scrolling combat text ON -- but Blizzard's own combat text is disabled in its" + .." options, so nothing will show in blizzard mode.") + else + print_(cfg.sct.enabled and "scrolling combat text ON." or "scrolling combat text OFF.") + end + elseif sub == "duration" then + local s = tonumber(val) + if not s or s < 0.5 or s > 4.0 then + print_("/vf sct duration -- 0.5 to 4.0, e.g. /vf sct duration 1.5") + else + cfg.sct.duration = s + print_("SCT fade duration set to "..s.."s.") + end + elseif sub == "shadow" then + if val == "on" then + cfg.sct.shadow = true + elseif val == "off" then + cfg.sct.shadow = false + else + print_("/vf sct shadow on|off") + return + end + if VampifySCT and VampifySCT.applyFont then VampifySCT.applyFont() end + print_(cfg.sct.shadow and "SCT text shadow ON." or "SCT text shadow OFF.") else - print_(cfg.sct.enabled and "scrolling combat text ON." or "scrolling combat text OFF.") + help() end elseif cmd == "sctmove" then if not (VampifySCT and VampifySCT.setMoveMode and VampifySCT.isMoveMode) then diff --git a/core/const.lua b/core/const.lua index 8bdd0b0..f9b5b3b 100644 --- a/core/const.lua +++ b/core/const.lua @@ -7,7 +7,7 @@ VampifyConst = {} local C = VampifyConst -C.VERSION = "0.3.0" +C.VERSION = "0.4.0" -- Empty a list buffer for reuse. THE table.setn IS THE POINT: in Lua 5.0 table.insert maintains an -- `n` field, and nil-ing the indices by hand does not reset it -- so the next round of inserts diff --git a/core/perhit.lua b/core/perhit.lua index 8beb4be..ac3e91f 100644 --- a/core/perhit.lua +++ b/core/perhit.lua @@ -140,13 +140,59 @@ function PH.pushLine(state, line) end end +-- The INDIVIDUAL source percentages of the hit, as "3/2/2/2". +-- +-- WHY IT HAD TO BE ADDED (offline analysis, 2026-08-30). The line carried only the SUMMED +-- percentage P and the source count n, and the server truncates every source SEPARATELY +-- (core/model.lua's header): max(1, floor(pct_i * damage / 100)) per source. From P and n alone +-- that is not reconstructable -- an offline reader has to approximate the list as n equal shares, +-- which is exactly VampifyModel.heal's documented-inexact compatibility shape. Measured against +-- the recorded pred_heal over 4907 windows that approximation is off by 4.7 %, and 4.7 % is the +-- same order as the effects the analysis is trying to separate. With the real list the offline +-- model becomes exact instead of approximate, and the AoE-ordering question core/model.lua's +-- header leaves open (damp the damage, or damp each source's heal) becomes decidable from +-- recorded data rather than needing a new in-game measurement. +-- +-- "%g", not "%d" or "%.2f": a source percentage is normally a small integer (3, 2, 2) and should +-- print as one, but nothing guarantees it -- a fractional percentage must survive rather than be +-- silently truncated into a wrong number. +-- +-- Buffer-recycled like every other reused list here (VampifyConst.resetList's comment): this runs +-- once per own hit, and a fresh table per hit is avoidable churn on a channel that is meant to run +-- permanently. The caller's list is READ ONLY -- and the result is a STRING, which matters: the +-- live source list is itself a recycled buffer (core/commands.lua's sourcePercents, refilled in +-- place on every gear change), so a row may not hold a reference to it. Serialising here, at +-- record time, is what keeps a buffered row from silently re-reading a list that has since changed. +local pctBuf = {} +function PH.formatPercents(list) + if not list then return "" end + local n = table.getn(list) + for i = table.getn(pctBuf), n + 1, -1 do pctBuf[i] = nil end + table.setn(pctBuf, n) + for i = 1, n do pctBuf[i] = string.format("%g", list[i] or 0) end + return table.concat(pctBuf, "/", 1, n) +end + +-- pcts / zone / cast are the three fields the offline analysis asked for (docs/ +-- VAMPIRISM-MODELL-STAND §4). All three are optional: an older caller that does not supply them +-- still produces a well-formed line ("", "?" and 0), so the export never depends on every wiring +-- site having been updated in the same change. +-- +-- cast=1 means a SPELL_GO_SELF for THIS spell id arrived shortly before this hit -- i.e. the player +-- CAST it. cast=0 means no cast announced it, which for a damaging spell is the signature of a +-- proc. That distinction is the whole point: hypothesis H2 says Vampirism is fed by the TRIGGER +-- PATH ("was this triggered by an aura") rather than by "is this an item proc", and the cast event +-- is the one observable in the client that separates the two paths for the same spell id. Without +-- it the offline classification has to be inferred from timing against Lightning Strike hits and +-- incoming damage, which is what the current analysis does and why it can only report AMBIG for +-- 13.4 % of Tidal Wave hits. function PH.formatHitLine(row) return string.format( "HIT|t=%.3f|src=%s|dmg=%d|aoe=%d|P=%.4f|n=%d|pred_heal=%.4f|hp_before=%s|hp_after=%s|" - .."hp_max=%s|acc_total=%.4f|crit=%d", + .."hp_max=%s|acc_total=%.4f|crit=%d|pcts=%s|zone=%s|cast=%d", row.t, row.src, row.dmg, (row.aoe and 1 or 0), row.P, row.n, row.pred_heal, tostring(row.hp_before), tostring(row.hp_after), tostring(row.hp_max), row.acc_total, - (row.crit and 1 or 0)) + (row.crit and 1 or 0), row.pcts or "", row.zone or "?", (row.cast and 1 or 0)) end function PH.formatIncomingLine(row) @@ -177,15 +223,23 @@ function PH.finalizePending(state, hpNow) return true end --- fields: t, src, dmg, aoe, P, n, pred_heal, acc_total, crit -- everything except the health --- readings, which this function supplies itself: hp_before = hpNow and hp_max = hpMaxNow are both --- stamped HERE, at the same point, and never touched again; hp_after comes later, at finalize. +-- fields: t, src, dmg, aoe, P, n, pred_heal, acc_total, crit, pcts, zone, cast -- everything +-- except the health readings, which this function supplies itself: hp_before = hpNow and +-- hp_max = hpMaxNow are both stamped HERE, at the same point, and never touched again; hp_after +-- comes later, at finalize. +-- +-- fields.pcts is the LIVE source-percent list and is serialised HERE, not held. The row can sit +-- pending (and then buffered) for a long time, and that list is a recycled buffer refilled in +-- place whenever gear changes -- keeping the reference would make an already-recorded hit report +-- whatever the source set happens to be at flush time. Same reasoning as the health readings +-- above: a value that describes THIS hit is captured at this hit. function PH.recordHit(state, fields, hpNow, tNow, hpMaxNow) PH.finalizePending(state, hpNow) state.pending = { t = fields.t, src = fields.src, dmg = fields.dmg, aoe = fields.aoe, P = fields.P, n = fields.n, pred_heal = fields.pred_heal, acc_total = fields.acc_total, crit = fields.crit, + pcts = PH.formatPercents(fields.pcts), zone = fields.zone, cast = fields.cast, hp_before = hpNow, hp_max = hpMaxNow, pendingAt = tNow, } state.totalHits = state.totalHits + 1 diff --git a/gui/options.lua b/gui/options.lua index f52878b..20be793 100644 --- a/gui/options.lua +++ b/gui/options.lua @@ -30,7 +30,7 @@ if CreateFrame then -- -- 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 + sliderPillH, sliderPanelBarH, sliderDuration local function db() return VampifyConfig.get() end @@ -247,6 +247,10 @@ if CreateFrame then -- W.SetSliderEnabled, which follows Blizzard's own hide-the-thumb recipe. W.SetSliderEnabled(sliderRise, posEnabled) W.SetSliderEnabled(sliderFont, posEnabled) + -- Fade duration is the same category as fontSize/rise above: it shapes OUR pooled + -- FontStrings only (gui/sct.lua's S.spawn reads sct.duration into s.dur), so it follows + -- the same posEnabled greying. + W.SetSliderEnabled(sliderDuration, 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 @@ -274,6 +278,12 @@ if CreateFrame then sliderPanelBarH:SetValue(v) getglobal("VampifyOptionsPanelBarHeightSliderText"):SetText(sliderPanelBarH.vfLabel .. ": " .. v) end + if sliderDuration then + local v = sliderDuration.vfGetter() + sliderDuration:SetValue(v) + getglobal("VampifyOptionsDurationSliderText"):SetText( + sliderDuration.vfLabel .. ": " .. string.format(sliderDuration.vfFmt or "%s", v)) + end end -- ------------------------------------------------------------------ @@ -284,7 +294,10 @@ if CreateFrame then local f = CreateFrame("Frame", "VampifyOptionsFrame", UIParent) f:SetWidth(340) - f:SetHeight(540) + f:SetHeight(608) -- +42 (Fade duration slider) +26 (Text shadow checkbox) over the + -- previous 540; fitHeight() below still re-measures against the + -- real info-panel position on every OnShow, so this is only the + -- pre-first-show estimate, not the operative bound. 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 @@ -593,27 +606,42 @@ if CreateFrame then -- ---- 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) + -- fmt (optional): a string.format pattern for the slider's own value, e.g. "%.1f" for a + -- fractional-second slider (Fade duration below, step 0.1). Omitted, values print with a + -- plain tostring -- unchanged behaviour for every integer-step slider that existed before it. + local function makeSlider(name, label, lo, hi, step, getter, setter, fmt) 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)) + getglobal(name .. "Low"):SetText(fmt and string.format(fmt, lo) or tostring(lo)) + getglobal(name .. "High"):SetText(fmt and string.format(fmt, hi) or tostring(hi)) local cap = getglobal(name .. "Text") W.ApplyFont(cap, 11) sl.vfLabel = label sl.vfSetter = setter sl.vfGetter = getter + sl.vfStep = step + sl.vfFmt = fmt 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) + -- OptionsSliderTemplate does not quantise for us in 1.12; round to the slider's own + -- step by hand. math.floor(v/step+0.5)*step generalises the old "nearest integer" + -- rounding (which was step-1-only, hardcoded) to the fractional step a duration-in- + -- seconds slider needs (0.1) -- and is a no-op for every existing step-1 slider. + v = math.floor(v / this.vfStep + 0.5) * this.vfStep + -- Round-trip a fractional value through its own format string (e.g. "%.1f") before + -- storing it. The multiply above leaves binary-float noise (1.2000000000000002), + -- which would both write an ugly value into SavedVariables and never compare equal + -- to the plain decimal literal a getter's own fallback or a test uses -- the + -- round-trip snaps it back to the canonical double for "1.2". No-op for integer + -- sliders (fmt is nil there). + local txt = this.vfFmt and string.format(this.vfFmt, v) or tostring(v) + if this.vfFmt then v = tonumber(txt) end this.vfSetter(v) - getglobal(this:GetName() .. "Text"):SetText(this.vfLabel .. ": " .. v) + getglobal(this:GetName() .. "Text"):SetText(this.vfLabel .. ": " .. txt) end) y = y - 42 return sl @@ -642,6 +670,24 @@ if CreateFrame then cfg.sct.rise = v end) + -- Fade duration -- how long a number stays on screen (gui/sct.lua's S.spawn does + -- "s.dur = sct.duration or DUR_FALLBACK" fresh on every spawn, not cached, so no applyX + -- entry point is needed here: the very next number picks up a changed value on its own, + -- same as the rise slider above. + sliderDuration = makeSlider("VampifyOptionsDurationSlider", "Fade duration", 0.5, 4.0, 0.1, + function() + local cfg = db() + return (cfg and cfg.sct and cfg.sct.duration) or 1.5 + end, + function(v) + local cfg = db() + if not (cfg and cfg.sct) then return end + cfg.sct.duration = v + end, + "%.1f") + W.AttachTooltip(sliderDuration, "How long a Vampify combat-text number stays on screen" + .. " before fading out, in seconds.") + -- 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", "" } @@ -672,6 +718,27 @@ if CreateFrame then end y = y - 28 + -- Text shadow -- the other half of "readable over anything" alongside the outline above. + -- Getter mirrors gui/sct.lua's own shadowOn(): default ON, an EXPLICIT false is what turns + -- it off (a missing/unmigrated key must not read as off). applyFont() re-stamps every + -- pooled FontString immediately, same as the outline cycle button above -- otherwise + -- already-spawned slots would keep the old shadow state until the pool cycled. + local cbShadow = CreateCheckbox(f, "Text shadow", + function() + local cfg = db() + local sct = cfg and cfg.sct + if sct and sct.shadow == false then return false end + return true + end, + function(v) + local cfg = db() + if cfg and cfg.sct then cfg.sct.shadow = v end + if VampifySCT and VampifySCT.applyFont then VampifySCT.applyFont() end + end) + place(cbShadow, -26) + W.AttachTooltip(cbShadow, "Drops a shadow behind Vampify's own combat-text numbers, for" + .. " readability over bright backgrounds. On by default.") + -- ---- 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 diff --git a/textures/hps_orb_fill.tga b/textures/hps_orb_fill.tga new file mode 100644 index 0000000..bff1326 Binary files /dev/null and b/textures/hps_orb_fill.tga differ diff --git a/textures/hps_orb_shade.tga b/textures/hps_orb_shade.tga new file mode 100644 index 0000000..f399074 Binary files /dev/null and b/textures/hps_orb_shade.tga differ diff --git a/textures/hps_orb_shine.tga b/textures/hps_orb_shine.tga new file mode 100644 index 0000000..5c6fca1 Binary files /dev/null and b/textures/hps_orb_shine.tga differ diff --git a/textures/pill_fill_cap.tga b/textures/pill_fill_cap.tga new file mode 100644 index 0000000..874dc4f Binary files /dev/null and b/textures/pill_fill_cap.tga differ diff --git a/textures/pill_shade_cap.tga b/textures/pill_shade_cap.tga new file mode 100644 index 0000000..6d55676 Binary files /dev/null and b/textures/pill_shade_cap.tga differ diff --git a/textures/pill_shine_cap.tga b/textures/pill_shine_cap.tga new file mode 100644 index 0000000..c2c6ac2 Binary files /dev/null and b/textures/pill_shine_cap.tga differ