2 Commits

Author SHA1 Message Date
ShempError ebd533ee7d TotemBar v0.4.1 2026-08-30 23:23:45 +02:00
ShempError 2cc7f89c61 TotemBar README: what's new at the top 2026-08-30 15:50:40 +02:00
4 changed files with 143 additions and 34 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 -16
View File
@@ -11,26 +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.2.0 · *The Pulse UI release* · 2026-07-10 ## What's new — v0.4.1 · 2026-08-31
- **Round floating slots** — the square boxes are gone; icons float in a - **A totem destroyed inside the recall attribution window stays dead.** A totem the enemy
subtle shadow-rim frame, with round hover/pressed states everywhere. destroyed in the ~0.4s window between a Totemic Recall wipe and its refusal being attributed
- **Time traffic light** — the outer ring drains with the totem's remaining no longer has its countdown resurrected by the refused-recall restore.
lifetime and shifts green → yellow → orange → red.
- **Pulse ring** — a thin element-colored inner arc fills toward the next
pulse and resets when it fires, animated per frame.
- **Server-accurate pulse timing** — intervals and first-tick behavior from
TurtleWoW server data (Tremor pulses every **4s** and immediately on
placement; Magma and the water totems first pulse one interval in). Water
totems re-anchor on the real observed mana/heal ticks.
- **Pulse waves & glow** — a ripple in the element color marks each pulse; a
halo builds just before it.
- **Bar layouts** — 1×6, 2×3 (elements as a 2×2 block) or 3×2.
- **Custom art set** — element glyphs for empty slots, drop-set icon, minimap
glyph, skinned options panel. All generated by scripts in `tools/`.
**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