Vampify v0.4.0
This commit is contained in:
@@ -1,4 +0,0 @@
|
|||||||
# Editor / OS noise
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
*.log
|
|
||||||
+23
-16
@@ -2,26 +2,20 @@
|
|||||||
|
|
||||||
All notable changes to Vampify are documented here.
|
All notable changes to Vampify are documented here.
|
||||||
|
|
||||||
## Unreleased
|
## 0.4.0 — 2026-08-30
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- MIT license, this changelog, and a README written for players rather than for the author.
|
- 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
|
||||||
### Fixed
|
cycle button. Both are also reachable from the command line: `/vf sct duration <seconds>` and
|
||||||
- Long lines in a row's detail panel are wrapped instead of being drawn over the lines below them.
|
`/vf sct shadow on|off`.
|
||||||
- Tooltips raised while the breakdown panel is open now appear above it rather than behind it.
|
- The glass-bar textures the 0.3.0 upload left out (`textures/hps_orb_*` and
|
||||||
- The whole bar can be dragged when it is unlocked. Several parts of it — the leftmost number, the
|
`textures/pill_*_cap`). Without them the bars drew without their fill, shading and gloss.
|
||||||
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.
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- The display bar is compact: the prefix and the session ratio are gone, and it shows the equipped
|
- The per-hit export carries three more fields for the offline analysis: each source's own
|
||||||
loadout percentage together with the number of sources it found.
|
percentage, the zone, and whether the hit followed a cast of the same spell (item and enchant
|
||||||
- The per-hit export records maximum health per hit, so an offline analysis can tell overheal apart
|
procs never do). All three are optional, so an older reader still parses the line.
|
||||||
from healing that never arrived.
|
|
||||||
|
|
||||||
## 0.3.0 — 2026-08-22
|
## 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.
|
- 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,
|
- AoE damping is applied only where a hit is proven to have struck two or more distinct targets,
|
||||||
rather than inferred from the spell.
|
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
|
### Added
|
||||||
- Per-hit debug export for measuring the per-source factor, with a persisted toggle and an options
|
- Per-hit debug export for measuring the per-source factor, with a persisted toggle and an options
|
||||||
checkbox.
|
checkbox.
|
||||||
- Spell exclusions: a spell can be stopped from counting as a Vampirism trigger, and the exclusion
|
- Spell exclusions: a spell can be stopped from counting as a Vampirism trigger, and the exclusion
|
||||||
survives a reload.
|
survives a reload.
|
||||||
|
- MIT license, this changelog, and a README written for players rather than for the author.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- The raw per-hit capture no longer depends on the watch-mode group guard.
|
- 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.
|
can actually trust, instead of firing on a quiet moment.
|
||||||
- An AoE correction removes the integers it actually credited rather than reconstructing them.
|
- 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.
|
- 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
|
## 0.2.0 — 2026-08-10
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
## Title: Vampify
|
## Title: Vampify
|
||||||
## Notes: Tracks the healing returned by the Vampirism item stat.
|
## Notes: Tracks the healing returned by the Vampirism item stat.
|
||||||
## Author: ShempError
|
## Author: ShempError
|
||||||
## Version: 0.3.0
|
## Version: 0.4.0
|
||||||
## SavedVariables: VampifyDB
|
## SavedVariables: VampifyDB
|
||||||
## SavedVariablesPerCharacter: VampifyCharDB
|
## SavedVariablesPerCharacter: VampifyCharDB
|
||||||
|
|
||||||
|
|||||||
@@ -117,6 +117,42 @@ function G.onDamage(fn) table.insert(listeners, fn) end
|
|||||||
local goSeen = false
|
local goSeen = false
|
||||||
local damageSeen = 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) --------
|
-- ---- 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
|
-- 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
|
-- 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.
|
-- exact same distinct-target proof a no-cast proc already has to provide.
|
||||||
goSeen = true
|
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
|
elseif event == "SPELL_DAMAGE_EVENT_SELF" then
|
||||||
local amount, tgt, spellId, caster = G.decodeSpell(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)
|
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.
|
-- Trust arg2 rather than the _SELF suffix, and never count a hit on ourselves.
|
||||||
|
|||||||
+71
-5
@@ -319,6 +319,28 @@ local lastHp, lastAt, lastExpected = nil, nil, nil
|
|||||||
local nextCheck = 0
|
local nextCheck = 0
|
||||||
local WATCH_INTERVAL = 15 -- slow on purpose: findings are read by a human, not a loop
|
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
|
-- print_ already prefixes "Vampify", so this must not repeat it -- the first field build did, and
|
||||||
-- the result read "Vampify Vampify watch:".
|
-- the result read "Vampify Vampify watch:".
|
||||||
--
|
--
|
||||||
@@ -485,6 +507,13 @@ if VampifyDamage.onDamage then
|
|||||||
pred_heal = healFloat,
|
pred_heal = healFloat,
|
||||||
acc_total = VampifyState.acc.total,
|
acc_total = VampifyState.acc.total,
|
||||||
crit = isCrit,
|
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"))
|
}, UnitHealth and UnitHealth("player"), GetTime(), UnitHealthMax and UnitHealthMax("player"))
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -823,6 +852,9 @@ if CreateFrame then
|
|||||||
f:RegisterEvent("PLAYER_CAMPING")
|
f:RegisterEvent("PLAYER_CAMPING")
|
||||||
f:RegisterEvent("PLAYER_QUITING")
|
f:RegisterEvent("PLAYER_QUITING")
|
||||||
f:RegisterEvent("LOGOUT_CANCEL")
|
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.
|
-- 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
|
-- Declared out here rather than beside the OnUpdate that uses it because the combat-end branch
|
||||||
@@ -860,8 +892,13 @@ if CreateFrame then
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if event == "ZONE_CHANGED_NEW_AREA" then
|
||||||
|
refreshZone()
|
||||||
|
return
|
||||||
|
end
|
||||||
if event == "PLAYER_LOGIN" or event == "PLAYER_ENTERING_WORLD" then
|
if event == "PLAYER_LOGIN" or event == "PLAYER_ENTERING_WORLD" then
|
||||||
if event == "PLAYER_ENTERING_WORLD" then wireSession() end
|
if event == "PLAYER_ENTERING_WORLD" then wireSession() end
|
||||||
|
refreshZone()
|
||||||
recompute()
|
recompute()
|
||||||
syncPerHit()
|
syncPerHit()
|
||||||
if VampifyMinimap and VampifyMinimap.update then VampifyMinimap.update() end
|
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 pos -- move the display back to its default position")
|
||||||
print_("/vf lock -- toggle frame dragging")
|
print_("/vf lock -- toggle frame dragging")
|
||||||
print_("/vf fct / sct -- toggle scrolling combat text (either engine)")
|
print_("/vf fct / sct -- toggle scrolling combat text (either engine)")
|
||||||
|
print_("/vf sct duration <s> -- 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 sctmove -- move our own SCT anchor (options window has the same button)")
|
||||||
print_("/vf options -- open the options window")
|
print_("/vf options -- open the options window")
|
||||||
print_("/vf source add <pct> -- add a manual source, if tooltip detection fails")
|
print_("/vf source add <pct> -- add a manual source, if tooltip detection fails")
|
||||||
@@ -1163,12 +1202,39 @@ SlashCmdList["VAMPIFY"] = function(msg)
|
|||||||
elseif cmd == "fct" or cmd == "sct" then
|
elseif cmd == "fct" or cmd == "sct" then
|
||||||
local cfg = VampifyConfig.get()
|
local cfg = VampifyConfig.get()
|
||||||
if not cfg or not cfg.sct then print_("config not loaded yet.") return end
|
if not cfg or not cfg.sct then print_("config not loaded yet.") return end
|
||||||
cfg.sct.enabled = not cfg.sct.enabled
|
-- Bare "/vf sct" / "/vf fct" keeps its original meaning (toggle enabled); "sct <sub> <val>"
|
||||||
if cfg.sct.enabled and cfg.sct.mode == "blizzard" and SHOW_COMBAT_TEXT ~= "1" then
|
-- is the same sub/val dispatch pattern /vf source and /vf exclude already use above. %S*
|
||||||
print_("scrolling combat text ON -- but Blizzard's own combat text is disabled in its"
|
-- rather than %d* for val: duration needs a decimal ("1.5"), shadow needs a word (on/off).
|
||||||
.." options, so nothing will show in blizzard mode.")
|
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 <seconds> -- 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
|
else
|
||||||
print_(cfg.sct.enabled and "scrolling combat text ON." or "scrolling combat text OFF.")
|
help()
|
||||||
end
|
end
|
||||||
elseif cmd == "sctmove" then
|
elseif cmd == "sctmove" then
|
||||||
if not (VampifySCT and VampifySCT.setMoveMode and VampifySCT.isMoveMode) then
|
if not (VampifySCT and VampifySCT.setMoveMode and VampifySCT.isMoveMode) then
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
VampifyConst = {}
|
VampifyConst = {}
|
||||||
local C = 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
|
-- 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
|
-- `n` field, and nil-ing the indices by hand does not reset it -- so the next round of inserts
|
||||||
|
|||||||
+59
-5
@@ -140,13 +140,59 @@ function PH.pushLine(state, line)
|
|||||||
end
|
end
|
||||||
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)
|
function PH.formatHitLine(row)
|
||||||
return string.format(
|
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|"
|
"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,
|
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,
|
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
|
end
|
||||||
|
|
||||||
function PH.formatIncomingLine(row)
|
function PH.formatIncomingLine(row)
|
||||||
@@ -177,15 +223,23 @@ function PH.finalizePending(state, hpNow)
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
-- fields: t, src, dmg, aoe, P, n, pred_heal, acc_total, crit -- everything except the health
|
-- fields: t, src, dmg, aoe, P, n, pred_heal, acc_total, crit, pcts, zone, cast -- everything
|
||||||
-- readings, which this function supplies itself: hp_before = hpNow and hp_max = hpMaxNow are both
|
-- except the health readings, which this function supplies itself: hp_before = hpNow and
|
||||||
-- stamped HERE, at the same point, and never touched again; hp_after comes later, at finalize.
|
-- 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)
|
function PH.recordHit(state, fields, hpNow, tNow, hpMaxNow)
|
||||||
PH.finalizePending(state, hpNow)
|
PH.finalizePending(state, hpNow)
|
||||||
state.pending = {
|
state.pending = {
|
||||||
t = fields.t, src = fields.src, dmg = fields.dmg, aoe = fields.aoe,
|
t = fields.t, src = fields.src, dmg = fields.dmg, aoe = fields.aoe,
|
||||||
P = fields.P, n = fields.n, pred_heal = fields.pred_heal,
|
P = fields.P, n = fields.n, pred_heal = fields.pred_heal,
|
||||||
acc_total = fields.acc_total, crit = fields.crit,
|
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,
|
hp_before = hpNow, hp_max = hpMaxNow, pendingAt = tNow,
|
||||||
}
|
}
|
||||||
state.totalHits = state.totalHits + 1
|
state.totalHits = state.totalHits + 1
|
||||||
|
|||||||
+76
-9
@@ -30,7 +30,7 @@ if CreateFrame then
|
|||||||
-- -- refreshSliders() below needs to push their persisted values in on OnShow, same as
|
-- -- refreshSliders() below needs to push their persisted values in on OnShow, same as
|
||||||
-- sliderFont/sliderRise already do.
|
-- sliderFont/sliderRise already do.
|
||||||
local swatch, btnMove, btnPosReset, posLabel, sliderFont, sliderRise, btnDemo, btnOutline,
|
local swatch, btnMove, btnPosReset, posLabel, sliderFont, sliderRise, btnDemo, btnOutline,
|
||||||
sliderPillH, sliderPanelBarH
|
sliderPillH, sliderPanelBarH, sliderDuration
|
||||||
|
|
||||||
local function db() return VampifyConfig.get() end
|
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, which follows Blizzard's own hide-the-thumb recipe.
|
||||||
W.SetSliderEnabled(sliderRise, posEnabled)
|
W.SetSliderEnabled(sliderRise, posEnabled)
|
||||||
W.SetSliderEnabled(sliderFont, 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
|
if btnDemo then W.SetButtonEnabled(btnDemo, posEnabled) end
|
||||||
-- The outline shapes OUR numbers only; Blizzard's engine draws its own.
|
-- The outline shapes OUR numbers only; Blizzard's engine draws its own.
|
||||||
if btnOutline then W.SetButtonEnabled(btnOutline, posEnabled) end
|
if btnOutline then W.SetButtonEnabled(btnOutline, posEnabled) end
|
||||||
@@ -274,6 +278,12 @@ if CreateFrame then
|
|||||||
sliderPanelBarH:SetValue(v)
|
sliderPanelBarH:SetValue(v)
|
||||||
getglobal("VampifyOptionsPanelBarHeightSliderText"):SetText(sliderPanelBarH.vfLabel .. ": " .. v)
|
getglobal("VampifyOptionsPanelBarHeightSliderText"):SetText(sliderPanelBarH.vfLabel .. ": " .. v)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
-- ------------------------------------------------------------------
|
-- ------------------------------------------------------------------
|
||||||
@@ -284,7 +294,10 @@ if CreateFrame then
|
|||||||
|
|
||||||
local f = CreateFrame("Frame", "VampifyOptionsFrame", UIParent)
|
local f = CreateFrame("Frame", "VampifyOptionsFrame", UIParent)
|
||||||
f:SetWidth(340)
|
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)
|
f:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
|
||||||
-- HIGH, deliberately NOT DIALOG. Blizzard's ColorPickerFrame lives at DIALOG, and a frame
|
-- 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
|
-- 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 -------------------------------------------------------------------
|
-- ---- SCT sliders -------------------------------------------------------------------
|
||||||
-- Replaces the X/Y entry boxes: raw anchor offsets meant nothing to a human, whereas these
|
-- 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.
|
-- 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")
|
local sl = CreateFrame("Slider", name, f, "OptionsSliderTemplate")
|
||||||
sl:SetWidth(200)
|
sl:SetWidth(200)
|
||||||
sl:SetHeight(16)
|
sl:SetHeight(16)
|
||||||
sl:SetMinMaxValues(lo, hi)
|
sl:SetMinMaxValues(lo, hi)
|
||||||
sl:SetValueStep(step)
|
sl:SetValueStep(step)
|
||||||
sl:SetPoint("TOPLEFT", f, "TOPLEFT", x + 6, y)
|
sl:SetPoint("TOPLEFT", f, "TOPLEFT", x + 6, y)
|
||||||
getglobal(name .. "Low"):SetText(tostring(lo))
|
getglobal(name .. "Low"):SetText(fmt and string.format(fmt, lo) or tostring(lo))
|
||||||
getglobal(name .. "High"):SetText(tostring(hi))
|
getglobal(name .. "High"):SetText(fmt and string.format(fmt, hi) or tostring(hi))
|
||||||
local cap = getglobal(name .. "Text")
|
local cap = getglobal(name .. "Text")
|
||||||
W.ApplyFont(cap, 11)
|
W.ApplyFont(cap, 11)
|
||||||
sl.vfLabel = label
|
sl.vfLabel = label
|
||||||
sl.vfSetter = setter
|
sl.vfSetter = setter
|
||||||
sl.vfGetter = getter
|
sl.vfGetter = getter
|
||||||
|
sl.vfStep = step
|
||||||
|
sl.vfFmt = fmt
|
||||||
sl:SetScript("OnValueChanged", function()
|
sl:SetScript("OnValueChanged", function()
|
||||||
local v = this:GetValue()
|
local v = this:GetValue()
|
||||||
-- OptionsSliderTemplate does not quantise for us in 1.12; round to the step so the
|
-- OptionsSliderTemplate does not quantise for us in 1.12; round to the slider's own
|
||||||
-- stored value stays a clean integer instead of 15.9999.
|
-- step by hand. math.floor(v/step+0.5)*step generalises the old "nearest integer"
|
||||||
v = math.floor(v + 0.5)
|
-- 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)
|
this.vfSetter(v)
|
||||||
getglobal(this:GetName() .. "Text"):SetText(this.vfLabel .. ": " .. v)
|
getglobal(this:GetName() .. "Text"):SetText(this.vfLabel .. ": " .. txt)
|
||||||
end)
|
end)
|
||||||
y = y - 42
|
y = y - 42
|
||||||
return sl
|
return sl
|
||||||
@@ -642,6 +670,24 @@ if CreateFrame then
|
|||||||
cfg.sct.rise = v
|
cfg.sct.rise = v
|
||||||
end)
|
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,
|
-- 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.
|
-- and the label always states the current one, so nothing is hidden behind a menu.
|
||||||
local OUTLINES = { "THICKOUTLINE", "OUTLINE", "" }
|
local OUTLINES = { "THICKOUTLINE", "OUTLINE", "" }
|
||||||
@@ -672,6 +718,27 @@ if CreateFrame then
|
|||||||
end
|
end
|
||||||
y = y - 28
|
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
|
-- ---- display size (2026-08-25, feature request: "two more sliders in the options that
|
||||||
-- determine the height of the bar and the detail bars") -----------------------------------
|
-- determine the height of the bar and the detail bars") -----------------------------------
|
||||||
-- Two sliders, same makeSlider() recipe as the SCT ones above, driving VampifyDisplay's own
|
-- Two sliders, same makeSlider() recipe as the SCT ones above, driving VampifyDisplay's own
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
Reference in New Issue
Block a user