Vampify v0.4.0

This commit is contained in:
2026-08-30 15:37:26 +02:00
parent ad50ebe636
commit d3ad691770
14 changed files with 271 additions and 41 deletions
+59 -5
View File
@@ -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