Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ebd533ee7d | |||
| 2cc7f89c61 |
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user