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.
## 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
### 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%">
## 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
subtle shadow-rim frame, with round hover/pressed states everywhere.
- **Time traffic light** — the outer ring drains with the totem's remaining
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/`.
- **A totem destroyed inside the recall attribution window stays dead.** A totem the enemy
destroyed in the ~0.4s window between a Totemic Recall wipe and its refusal being attributed
no longer has its countdown resurrected by the refused-recall restore.
**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
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
+1 -1
View File
@@ -2,7 +2,7 @@
## Title: TotemBar
## Notes: Shaman totem bar - pick one totem per element, cast it, or spam-cycle all four from a macro.
## Author: ShempError
## Version: 0.4.0
## Version: 0.4.1
## SavedVariables: TotemBarDB
core\totemdata.lua
+128 -17
View File
@@ -1307,6 +1307,21 @@ if CreateFrame then
TotemBar.TOTEM_ELEMENTS, name, owner, playerName)
if element then
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
return
end
@@ -1315,22 +1330,20 @@ if CreateFrame then
local element = TotemBar.elementForDiesLineCandidate(TotemBar.activeTotems,
TotemBar.TOTEM_ELEMENTS, diesName)
if element then
local rec = TotemBar.activeTotems[element]
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
if TotemBar.verifyDiesLineDestruction(TotemBar.activeTotems[element]) then
TotemBar.evictDestroyedTotem(element)
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
return
@@ -1346,6 +1359,7 @@ if CreateFrame then
-- events cannot cause a silent miss.
local element = TotemBar.elementForGuid(TotemBar.activeTotems,
TotemBar.TOTEM_ELEMENTS, arg1, TotemBar.guidsEqual)
local unitName, ownerName, playerName = nil, nil, nil
if not element and arg1 and type(UnitName) == "function" then
-- GUID-free fallback (2026-08-20 event tap: UnitName/owner
-- 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
-- pair) -- covers a totem whose GUID was never latched by
-- either latch path at all.
local unitName = UnitName(arg1)
local ownerName = UnitName(arg1 .. "owner")
local playerName = UnitName("player")
unitName = UnitName(arg1)
ownerName = UnitName(arg1 .. "owner")
playerName = UnitName("player")
element = TotemBar.elementForOwnedTotemName(TotemBar.activeTotems,
TotemBar.TOTEM_ELEMENTS, unitName, ownerName, playerName)
end
@@ -1368,6 +1382,29 @@ if CreateFrame then
-- moment later" bug for the one case it exists to short-
-- circuit fastest.
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
return
end
@@ -1514,6 +1551,80 @@ function TotemBar.restoreRecalledTotems()
return restored
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
-- 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