Files
2026-08-30 15:37:01 +02:00

327 lines
12 KiB
Lua

-- TotemBar - core/manacost.lua
-- Mana-cost reading + refund/duration helpers. The PURE functions here are
-- offline-tested; the WoW-API pieces (hidden-tooltip scan, talent scan, refund
-- learner) are appended in a later task and are guarded so this file loads
-- fine under plain Lua for the tests.
TotemBar = TotemBar or {}
-- Pure: mana cost from a tooltip line like "155 Mana" -> 155, else nil.
function TotemBar.parseManaCost(text)
if not text then
return nil
end
-- Anchor to the "Mana" keyword so a numeric line like "30 sec cooldown"
-- can't be mistaken for the cost (don't rely on tooltip line ordering).
local _, _, n = string.find(text, "^(%d+) [Mm]ana")
if n then
return tonumber(n)
end
return nil
end
-- Pure: sum costFn(chosen[element]) over the elements that have a chosen totem.
function TotemBar.sumChosenCost(chosen, elements, costFn)
local total = 0
if not chosen then
return 0
end
for i = 1, table.getn(elements) do
local name = chosen[elements[i]]
if name then
local c = costFn(name)
if c then
total = total + c
end
end
end
return total
end
-- Pure: sum cost of active totems that are still out (remaining > 0).
function TotemBar.sumActiveCost(activeTotems, elements, now, costFn, remainingFn)
local total = 0
if not activeTotems then
return 0
end
for i = 1, table.getn(elements) do
local rec = activeTotems[elements[i]]
if rec then
local rem = remainingFn(rec.start, rec.duration, now)
if rem and rem > 0 then
local c = costFn(rec.totemName)
if c then
total = total + c
end
end
end
end
return total
end
-- Pure: is a KNOWN mana shortfall in front of us? Both halves must be known
-- numbers -- an unresolved cost (the normal state until the tooltip scan runs,
-- and permanently for a totem that isn't in this character's book) is "I don't
-- know", never "too expensive". Greying a bar on "I don't know" would grey the
-- whole bar at login; refusing to record a timer on it would silently drop the
-- countdown of a totem that is standing. Both callers therefore fail OPEN here,
-- the same policy as confidentNoneOut/recallReady in core/cast.lua.
function TotemBar.notEnoughMana(cost, mana)
if not cost or not mana then
return false
end
return mana < cost
end
-- Clearcasting (Elemental Focus) used to exempt every mana verdict here. That was
-- WRONG and is gone (2026-08-28, player-reported): Elemental Focus zeroes the next
-- DAMAGE spell, not a totem summon, so a totem costs full price while Clearcasting
-- is up. Because totems are the only thing this addon casts, the exemption
-- disabled the mana gate and the out-of-mana dim outright whenever the buff was
-- up. TotemBar.hasClearcasting/CLEARCAST_ICON were removed with it -- the icon was
-- never verified in-game and now has no consumer. TotemBar.hasBuffWithIcon stays:
-- the out-of-range tint still uses it.
-- Live mana verdict for one totem by name: true only when its cost is known,
-- the player's mana is below it, and no Clearcasting is up. Reads the cached
-- tooltip cost (see getTotemManaCost), so this is a table lookup plus one
-- UnitMana call -- cheap enough for the 5Hz display tick that calls it.
function TotemBar.totemOutOfMana(name)
if not name or type(UnitMana) ~= "function" then
return false
end
return TotemBar.notEnoughMana(TotemBar.getTotemManaCost(name), UnitMana("player"))
end
-- Pure: floored refund.
function TotemBar.refundAmount(pct, activeCost)
if not pct or not activeCost then
return 0
end
return math.floor(pct * activeCost)
end
-- Pure: learn refund pct from an observed mana gain; nil if out of sane range.
function TotemBar.learnRefundPct(manaGained, activeCost)
if not manaGained or not activeCost or activeCost <= 0 then
return nil
end
local pct = manaGained / activeCost
if pct < 0.05 or pct > 1.0 then
return nil
end
return pct
end
-- Pure: helpful totems get Totemic Mastery's +20% duration.
function TotemBar.durationWithMastery(baseDuration, isHelpful, hasMastery)
if hasMastery and isHelpful then
return baseDuration * 1.2
end
return baseDuration
end
-- The pure fire-DAMAGE totems do NOT get the +20% helpful-totem duration.
-- Everything else is treated as helpful. VERIFY in-game and adjust.
TotemBar.NON_HELPFUL_TOTEMS = {
["Searing Totem"] = true,
["Magma Totem"] = true,
["Fire Nova Totem"] = true,
}
function TotemBar.isHelpfulTotem(name)
if not name then
return false
end
if TotemBar.NON_HELPFUL_TOTEMS[name] then
return false
end
return true
end
-- Pure: brings a pfUI-libtotem-reported duration onto the same scale as the
-- one recordCast stores. libtotem serves FLAT book durations; own tracking
-- applies Totemic Mastery's +20% to helpful totems. ui.lua's timer prefers
-- libtotem while it reports the slot active and falls back to own tracking
-- once libtotem evicts it -- so at the book duration the display flipped
-- source and the countdown jumped back UP (...3, 2, 1, 24) with the duration
-- ring re-filling, instead of running out. Scaling both sides by the same
-- factor makes them agree on the endpoint.
function TotemBar.gtiDurationWithMastery(gtiDuration, gtiName, hasMastery)
if not gtiDuration then
return nil
end
return TotemBar.durationWithMastery(gtiDuration, TotemBar.isHelpfulTotem(gtiName), hasMastery)
end
-- ===== WoW-API layer (not offline-executed) =====
-- TotemBar.findHighestRankSlot(name) used to be defined here as its own
-- fresh linear GetSpellName() scan per call. It's now a cache-backed
-- accessor provided by core/spellindex.lua (loaded earlier in the TOC) --
-- same name, same signature, same resolved slot (the HIGHEST known rank,
-- which is the one CastSpellByName actually casts; a first-match scan
-- would return rank 1 -> cost far too low, the bug behind "the mana
-- values are wrong"). Shared: ui.lua's tooltips resolve through this too -
-- SetSpell with a first-name-match slot shows the RANK 1 tooltip (wrong
-- mana/duration).
local scanTip = nil
local manaCache = {} -- name -> cost (only positive results cached)
-- Reads a totem's mana cost via a hidden tooltip scan (no GetSpellManaCost on
-- 1.12). Reflects cost talents (Tidal Focus, Restorative Totems) automatically.
-- Cached by name; cache cleared on SPELLS_CHANGED (talent/rank changes).
function TotemBar.getTotemManaCost(name)
if not name then return nil end
if manaCache[name] then return manaCache[name] end
local idx = TotemBar.findHighestRankSlot(name)
if not idx then return nil end
if not scanTip then
scanTip = CreateFrame("GameTooltip", "TotemBarScanTooltip", nil, "GameTooltipTemplate")
end
scanTip:SetOwner(WorldFrame, "ANCHOR_NONE")
scanTip:ClearLines()
scanTip:SetSpell(idx, BOOKTYPE_SPELL)
local cost = nil
local lines = scanTip:NumLines() or 0
for i = 1, lines do
local fs = getglobal("TotemBarScanTooltipTextLeft" .. i)
local text = fs and fs:GetText()
local c = TotemBar.parseManaCost(text)
if c then
cost = c
break
end
end
if cost then manaCache[name] = cost end
return cost
end
-- Dev aid (/tb manadump): dump the RAW mana-cost scan for the chosen totems +
-- Totemic Recall to <WoW>\imports\totembar_manadump.txt, so the resolved
-- slot/rank, every tooltip line, and the parsed cost can be verified
-- off-client (ground truth for the mana-cost feature). Chat fallback if no
-- SuperWoW ExportFile.
function TotemBar.dumpManaScan()
local els = TotemBar.TOTEM_ELEMENTS
local chosen = (TotemBarDB and TotemBarDB.chosen) or {}
local list = {}
for i = 1, table.getn(els) do
local nm = chosen[els[i]]
if nm then list[table.getn(list) + 1] = nm end
end
list[table.getn(list) + 1] = "Totemic Recall"
if not scanTip then
scanTip = CreateFrame("GameTooltip", "TotemBarScanTooltip", nil, "GameTooltipTemplate")
end
local out = "TotemBar mana scan dump\n"
local total = 0
for i = 1, table.getn(list) do
local nm = list[i]
local idx = TotemBar.findHighestRankSlot(nm)
out = out .. "\n[" .. nm .. "] slot=" .. tostring(idx)
if idx then
local rn, rr = GetSpellName(idx, BOOKTYPE_SPELL)
out = out .. " resolved='" .. tostring(rn) .. "' rank='" .. tostring(rr) .. "'\n"
scanTip:SetOwner(WorldFrame, "ANCHOR_NONE")
scanTip:ClearLines()
scanTip:SetSpell(idx, BOOKTYPE_SPELL)
local lines = scanTip:NumLines() or 0
local cost = nil
for L = 1, lines do
local fs = getglobal("TotemBarScanTooltipTextLeft" .. L)
local text = fs and fs:GetText()
out = out .. " L" .. L .. ": " .. tostring(text) .. "\n"
if not cost then
local c = TotemBar.parseManaCost(text)
if c then cost = c end
end
end
out = out .. " parsedCost=" .. tostring(cost) .. "\n"
if cost and nm ~= "Totemic Recall" then total = total + cost end
else
out = out .. " (not found in spellbook)\n"
end
end
out = out .. "\nsumChosenCost(live)=" .. tostring(
TotemBar.sumChosenCost(chosen, els, TotemBar.getTotemManaCost)) .. "\n"
out = out .. "dumpTotal(chosen)=" .. total .. "\n"
if ExportFile then
ExportFile("totembar_manadump", out)
end
if DEFAULT_CHAT_FRAME then
DEFAULT_CHAT_FRAME:AddMessage(
"TotemBar: mana dump written (imports\\totembar_manadump.txt). chosen sum=" .. total)
end
end
-- Totemic Mastery (TWoW: +20% helpful-totem duration): cached scan of the
-- talent trees by name.
local masteryCached = false
local function scanMastery()
masteryCached = false
local tabs = (GetNumTalentTabs and GetNumTalentTabs()) or 0
for tab = 1, tabs do
local num = GetNumTalents(tab)
for i = 1, num do
local tname, _, _, _, rank = GetTalentInfo(tab, i)
if tname == "Totemic Mastery" and rank and rank > 0 then
masteryCached = true
end
end
end
end
function TotemBar.hasTotemicMastery()
return masteryCached
end
-- Recall-refund auto-learn. Snapshot the summed cost of the totems currently
-- out just before a DELIBERATE Totemic Recall; when the mana-gain message
-- arrives shortly after, learn the real refund %.
TotemBar.recallPendingCost = 0
local recallExpectUntil = 0
function TotemBar.snapshotRecallCost()
TotemBar.recallPendingCost = TotemBar.sumActiveCost(
TotemBar.activeTotems, TotemBar.TOTEM_ELEMENTS, GetTime(),
TotemBar.getTotemManaCost, TotemBar.remaining)
recallExpectUntil = GetTime() + 2
end
-- Events: refresh mastery, clear the cost cache on spell changes, and learn
-- the refund % from the recall mana-gain message.
-- Guarded: CreateFrame is nil under plain offline Lua, so this whole block is
-- skipped there (loadfile/dofile must not error for the test suite).
if CreateFrame then
local mcEvents = CreateFrame("Frame", "TotemBarManaCostEventFrame", UIParent)
mcEvents:RegisterEvent("PLAYER_ENTERING_WORLD")
mcEvents:RegisterEvent("CHARACTER_POINTS_CHANGED")
mcEvents:RegisterEvent("SPELLS_CHANGED")
mcEvents:RegisterEvent("CHAT_MSG_SPELL_SELF_BUFF")
mcEvents:SetScript("OnEvent", function()
if event == "PLAYER_ENTERING_WORLD" or event == "CHARACTER_POINTS_CHANGED" then
scanMastery()
elseif event == "SPELLS_CHANGED" then
manaCache = {}
scanMastery()
elseif event == "CHAT_MSG_SPELL_SELF_BUFF" then
if GetTime() > recallExpectUntil then return end
local msg = arg1
if not msg then return end
-- Learn only from a Totemic Recall mana-gain within the window.
-- NOTE: exact message text is locale/format dependent - VERIFY in-game
-- (adjust the "Totemic Recall" + number pattern if needed).
if string.find(msg, "Totemic Recall") then
local _, _, num = string.find(msg, "(%d+)")
local gained = tonumber(num)
local pct = TotemBar.learnRefundPct(gained, TotemBar.recallPendingCost)
if pct and TotemBarDB then
TotemBarDB.recallRefundPct = pct
end
end
end
end)
end