TotemBar v0.4.1

This commit is contained in:
2026-08-30 23:23:45 +02:00
parent 2cc7f89c61
commit ebd533ee7d
4 changed files with 143 additions and 26 deletions
+7
View File
@@ -2,6 +2,13 @@
All notable changes to TotemBar are documented here. All notable changes to TotemBar are documented here.
## v0.4.1 — 2026-08-31
### Fixed
- A totem destroyed by the enemy inside the ~0.4s window between a Totemic Recall wipe and its
refusal being attributed now stays dead instead of having its countdown resurrected by the
refused-recall restore (`core/cast.lua`).
## v0.4.0 — 2026-08-30 ## v0.4.0 — 2026-08-30
### Added ### Added
+7 -8
View File
@@ -11,18 +11,17 @@ server data, not guesswork. No dependencies; adopts the
<img src="screenshots/pulse-ui.png" alt="TotemBar in action: round slots with duration and pulse rings, totems placed in the world" width="100%"> <img src="screenshots/pulse-ui.png" alt="TotemBar in action: round slots with duration and pulse rings, totems placed in the world" width="100%">
## What's new — v0.4.0 · 2026-08-30 ## What's new — v0.4.1 · 2026-08-31
- **Destroyed totems are detected.** A totem killed before its timer ran out used to keep - **A totem destroyed inside the recall attribution window stays dead.** A totem the enemy
counting down; now the death line (and, with SuperWoW/nampower, the cast and death events) destroyed in the ~0.4s window between a Totemic Recall wipe and its refusal being attributed
clears its countdown, ring, pulse and range tint. Live retest still pending. no longer has its countdown resurrected by the refused-recall restore.
- **No phantom countdown on a cast refused for mana.** The Clearcasting exemption never
applied to totems and had silently disabled the mana gate.
- **A refused Totemic Recall keeps your timers.** Only the Recall that actually went out clears
them; a press the server rejects restores what it had cleared.
**Version history** — details in [CHANGELOG.md](CHANGELOG.md): **Version history** — details in [CHANGELOG.md](CHANGELOG.md):
- **v0.4.1** (2026-08-31) — a totem destroyed by the enemy inside the recall attribution
window now stays dead instead of coming back when a refused Totemic Recall restores its
timers.
- **v0.4.0** (2026-08-30) — a totem destroyed before its timer ran out is now - **v0.4.0** (2026-08-30) — a totem destroyed before its timer ran out is now
detected and cleared (countdown, ring, pulse and range tint together) instead detected and cleared (countdown, ring, pulse and range tint together) instead
of counting down as if it still stood; a totem cast refused for lack of mana of counting down as if it still stood; a totem cast refused for lack of mana
+1 -1
View File
@@ -2,7 +2,7 @@
## Title: TotemBar ## Title: TotemBar
## Notes: Shaman totem bar - pick one totem per element, cast it, or spam-cycle all four from a macro. ## Notes: Shaman totem bar - pick one totem per element, cast it, or spam-cycle all four from a macro.
## Author: ShempError ## Author: ShempError
## Version: 0.4.0 ## Version: 0.4.1
## SavedVariables: TotemBarDB ## SavedVariables: TotemBarDB
core\totemdata.lua core\totemdata.lua
+128 -17
View File
@@ -1307,6 +1307,21 @@ if CreateFrame then
TotemBar.TOTEM_ELEMENTS, name, owner, playerName) TotemBar.TOTEM_ELEMENTS, name, owner, playerName)
if element then if element then
TotemBar.evictDestroyedTotem(element) TotemBar.evictDestroyedTotem(element)
return
end
-- Recall-attribution race (see "Recall-attribution vs.
-- destruction race" above): activeTotems has nothing for
-- this element because a recall wipe already cleared it and
-- is still awaiting attribution -- re-run the same owner/
-- name match against that pending snapshot so a later
-- restoreRecalledTotems doesn't bring the dead totem back.
local snapshotRecs = TotemBar.pendingRecallSnapshotRecs()
if snapshotRecs then
local snapElement = TotemBar.elementForOwnedTotemName(snapshotRecs,
TotemBar.TOTEM_ELEMENTS, name, owner, playerName)
if snapElement then
TotemBar.evictFromRecallSnapshot(snapElement)
end
end end
return return
end end
@@ -1315,22 +1330,20 @@ if CreateFrame then
local element = TotemBar.elementForDiesLineCandidate(TotemBar.activeTotems, local element = TotemBar.elementForDiesLineCandidate(TotemBar.activeTotems,
TotemBar.TOTEM_ELEMENTS, diesName) TotemBar.TOTEM_ELEMENTS, diesName)
if element then if element then
local rec = TotemBar.activeTotems[element] if TotemBar.verifyDiesLineDestruction(TotemBar.activeTotems[element]) then
local exists, guid, health, deadOrGhost = nil, nil, nil, nil
if type(UnitExists) == "function" then
exists, guid = UnitExists(rec.guid)
end
if exists then
if type(UnitHealth) == "function" then
health = UnitHealth(rec.guid)
end
if type(UnitIsDeadOrGhost) == "function" then
deadOrGhost = UnitIsDeadOrGhost(rec.guid)
end
end
if TotemBar.totemDestroyed(exists, health, deadOrGhost) then
TotemBar.evictDestroyedTotem(element) TotemBar.evictDestroyedTotem(element)
end end
return
end
-- Same recall-attribution race as the owned-name branch
-- above, for the generic "<Name> dies." fallback.
local snapshotRecs = TotemBar.pendingRecallSnapshotRecs()
if snapshotRecs then
local snapElement = TotemBar.elementForDiesLineCandidate(snapshotRecs,
TotemBar.TOTEM_ELEMENTS, diesName)
if snapElement and TotemBar.verifyDiesLineDestruction(snapshotRecs[snapElement]) then
TotemBar.evictFromRecallSnapshot(snapElement)
end
end end
end end
return return
@@ -1346,6 +1359,7 @@ if CreateFrame then
-- events cannot cause a silent miss. -- events cannot cause a silent miss.
local element = TotemBar.elementForGuid(TotemBar.activeTotems, local element = TotemBar.elementForGuid(TotemBar.activeTotems,
TotemBar.TOTEM_ELEMENTS, arg1, TotemBar.guidsEqual) TotemBar.TOTEM_ELEMENTS, arg1, TotemBar.guidsEqual)
local unitName, ownerName, playerName = nil, nil, nil
if not element and arg1 and type(UnitName) == "function" then if not element and arg1 and type(UnitName) == "function" then
-- GUID-free fallback (2026-08-20 event tap: UnitName/owner -- GUID-free fallback (2026-08-20 event tap: UnitName/owner
-- DO resolve at the exact death instant on this client, for -- DO resolve at the exact death instant on this client, for
@@ -1353,9 +1367,9 @@ if CreateFrame then
-- see the CHAT_MSG_COMBAT_FRIENDLY_DEATH/UNIT_DIED fixture -- see the CHAT_MSG_COMBAT_FRIENDLY_DEATH/UNIT_DIED fixture
-- pair) -- covers a totem whose GUID was never latched by -- pair) -- covers a totem whose GUID was never latched by
-- either latch path at all. -- either latch path at all.
local unitName = UnitName(arg1) unitName = UnitName(arg1)
local ownerName = UnitName(arg1 .. "owner") ownerName = UnitName(arg1 .. "owner")
local playerName = UnitName("player") playerName = UnitName("player")
element = TotemBar.elementForOwnedTotemName(TotemBar.activeTotems, element = TotemBar.elementForOwnedTotemName(TotemBar.activeTotems,
TotemBar.TOTEM_ELEMENTS, unitName, ownerName, playerName) TotemBar.TOTEM_ELEMENTS, unitName, ownerName, playerName)
end end
@@ -1368,6 +1382,29 @@ if CreateFrame then
-- moment later" bug for the one case it exists to short- -- moment later" bug for the one case it exists to short-
-- circuit fastest. -- circuit fastest.
TotemBar.evictDestroyedTotem(element) TotemBar.evictDestroyedTotem(element)
return
end
-- Recall-attribution race (see "Recall-attribution vs.
-- destruction race" above): re-run both matches against the
-- pending recall snapshot, reusing whatever UnitName lookups
-- already ran above instead of re-querying the (by-now-gone)
-- unit a second time.
local snapshotRecs = TotemBar.pendingRecallSnapshotRecs()
if snapshotRecs then
local snapElement = TotemBar.elementForGuid(snapshotRecs,
TotemBar.TOTEM_ELEMENTS, arg1, TotemBar.guidsEqual)
if not snapElement and arg1 and type(UnitName) == "function" then
if not unitName then
unitName = UnitName(arg1)
ownerName = UnitName(arg1 .. "owner")
playerName = UnitName("player")
end
snapElement = TotemBar.elementForOwnedTotemName(snapshotRecs,
TotemBar.TOTEM_ELEMENTS, unitName, ownerName, playerName)
end
if snapElement then
TotemBar.evictFromRecallSnapshot(snapElement)
end
end end
return return
end end
@@ -1514,6 +1551,80 @@ function TotemBar.restoreRecalledTotems()
return restored return restored
end end
-- ===== Recall-attribution vs. destruction race =====
--
-- recallWipeActiveTotems above clears TotemBar.activeTotems immediately and
-- keeps the pre-wipe records in TotemBar.lastRecallWipe.recs until the recall
-- is attributed. If a totem that was JUST wiped dies server-side inside that
-- same CAST_FAIL_WINDOW, the destruction handlers below (CHAT_MSG_COMBAT_
-- FRIENDLY_DEATH / UNIT_DIED) look it up in TotemBar.activeTotems -- which is
-- already empty for it -- so the eviction is a silent no-op and sets no
-- tombstone. If the recall is THEN attributed as refused, restoreRecalledTotems
-- puts the snapshot record back and the dead totem's countdown resumes. These
-- two helpers let the destruction handlers reach into the still-pending
-- snapshot with the exact same name/guid matches they already run against
-- activeTotems, so a totem that died mid-attribution stays dead either way.
-- The pending recall snapshot's records table, or nil if there is no recall
-- wipe awaiting attribution right now (none at all, or it is already older
-- than CAST_FAIL_WINDOW -- same staleness rule restoreRecalledTotems uses;
-- an old snapshot means any subsequent event is unrelated to it).
function TotemBar.pendingRecallSnapshotRecs()
local wiped = TotemBar.lastRecallWipe
if not wiped or not wiped.at or not wiped.recs then
return nil
end
if (GetTime() - wiped.at) > TotemBar.CAST_FAIL_WINDOW then
return nil
end
return wiped.recs
end
-- Evicts `element` from the pending recall snapshot (see above) instead of
-- from TotemBar.activeTotems -- the wipe already cleared that table, so
-- TotemBar.evictDestroyedTotem would no-op here. Removes the record from
-- TotemBar.lastRecallWipe.recs so a later restoreRecalledTotems leaves this
-- element alone, and tombstones it exactly like evictDestroyedTotem does, so
-- pfUI's libtotem can't resurrect the display for it either.
function TotemBar.evictFromRecallSnapshot(element)
local wiped = TotemBar.lastRecallWipe
if not wiped or not wiped.recs then
return
end
local rec = wiped.recs[element]
if not rec then
return
end
if rec.start and rec.duration then
TotemBar.destroyedTombstone[element] = rec.start + rec.duration
end
wiped.recs[element] = nil
end
-- Pure-ish (queries the live unit): runs the SAME "already exists, and either
-- zero health or dead-or-ghost" verification the CHAT_MSG_COMBAT_FRIENDLY_
-- DEATH generic "<Name> dies." fallback used inline before this was pulled
-- out -- shared now so the recall-snapshot branch below runs the identical
-- check instead of a second hand-copied one.
function TotemBar.verifyDiesLineDestruction(rec)
if not rec then
return false
end
local exists, guid, health, deadOrGhost = nil, nil, nil, nil
if type(UnitExists) == "function" then
exists, guid = UnitExists(rec.guid)
end
if exists then
if type(UnitHealth) == "function" then
health = UnitHealth(rec.guid)
end
if type(UnitIsDeadOrGhost) == "function" then
deadOrGhost = UnitIsDeadOrGhost(rec.guid)
end
end
return TotemBar.totemDestroyed(exists, health, deadOrGhost)
end
-- Is at least one totem currently out? Used to avoid wasting Totemic Recall's -- Is at least one totem currently out? Used to avoid wasting Totemic Recall's
-- own 6-second cooldown on a no-op cast: recalling with nothing out still puts -- own 6-second cooldown on a no-op cast: recalling with nothing out still puts
-- Recall on cooldown, so a fresh set placed right after can't be recalled for -- Recall on cooldown, so a fresh set placed right after can't be recalled for