- Find Life Tap ranks by spell ID (FindSpellBookSlotByID) with name scan fallback - /slt verify [adopt|reset] compares BASE table to C_Spell.GetSpellDescription - Macro buttons with /slt cast show the current rank (C_Macro.SetMacroDisplay) - Debug output of raw combat log values to confirm powerType position - Options: macro display checkbox, taller standalone window Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019W3rwwaFE77MpiHiYhRwLT
641 lines
26 KiB
Lua
641 lines
26 KiB
Lua
-- ScaledLifeTap 4.2
|
|
-- Original by Kimilly (thanks to Aredia of Gilneas for the macro and research).
|
|
-- Casts the highest Life Tap rank whose mana return fits in your missing mana.
|
|
--
|
|
-- Macro: /slt cast (or the old /script ScaledLifeTap_CastLifeTap() )
|
|
|
|
local L = SCALEDLIFETAP_L
|
|
local Scanner = ScaledLifeTap_Scanner
|
|
|
|
-- Base health converted per rank, and spell damage coefficient per rank.
|
|
-- Coefficient is 0.8 reduced by the sub-level-20 penalty for ranks 1 (lvl 6) and 2 (lvl 16).
|
|
local BASE = { 30, 75, 140, 220, 310, 424 }
|
|
local COEF = { 0.38, 0.68, 0.8, 0.8, 0.8, 0.8 }
|
|
|
|
-- Spell ID of each Life Tap rank. With ClassicAPI these find the spellbook slot
|
|
-- directly (no rank text or spell name needed) and identify the spell in the combat log.
|
|
local RANK_SPELL_IDS = { 1454, 1455, 1456, 11687, 11688, 11689 }
|
|
local NUM_RANKS = table.getn(RANK_SPELL_IDS)
|
|
local ID_TO_RANK = {}
|
|
for r = 1, NUM_RANKS do ID_TO_RANK[RANK_SPELL_IDS[r]] = r end
|
|
|
|
local DEFAULTS = {
|
|
overtap = true, -- cast rank 1 if no rank fits but mana is not full
|
|
debug = false,
|
|
reserve = 0, -- never tap below this % of max health
|
|
bonus = 0, -- extra spell damage to add (e.g. elixirs/buffs the scanner can't see)
|
|
source = "auto", -- "auto" = ClassicAPI > Nampower > built-in; or force one (incl. bonusscanner)
|
|
calibrate = true, -- learn a mana correction from the actual mana each tap gives
|
|
gainFactor = 1,
|
|
samples = 0,
|
|
macroDisplay = true, -- ClassicAPI: show the rank that would be cast on /slt cast macro buttons
|
|
verifyShown = false, -- the "spell data disagrees with built-in numbers" notice was shown once
|
|
-- baseOverride = { [rank] = health } is created by /slt verify adopt
|
|
}
|
|
|
|
local spellIndex = {} -- [rank] = spellbook index
|
|
local maxRank = 0
|
|
local spellsDirty = true
|
|
local talentMult, talentDirty = 1, true
|
|
|
|
local function msg(text) DEFAULT_CHAT_FRAME:AddMessage("|cff9482c9ScaledLifeTap:|r " .. text) end
|
|
local function dbg(text) if SLT_Config and SLT_Config.debug then msg(text) end end
|
|
|
|
-- Base health for a rank: the built-in table unless /slt verify adopt replaced it.
|
|
local function baseOf(r)
|
|
local o = SLT_Config and SLT_Config.baseOverride
|
|
return (o and o[r]) or BASE[r]
|
|
end
|
|
|
|
local function isLifeTapId(id)
|
|
id = tonumber(id)
|
|
return id ~= nil and ID_TO_RANK[id] ~= nil
|
|
end
|
|
|
|
---------------------------------------------------------------------------
|
|
-- spellbook / talents (cached, refreshed on events)
|
|
---------------------------------------------------------------------------
|
|
-- Name scan: works everywhere, depends on the localized spell name and rank text.
|
|
local function scanSpellbookByName()
|
|
local i = 1
|
|
while true do
|
|
local name, rankText = GetSpellName(i, BOOKTYPE_SPELL)
|
|
if not name then break end -- end of spellbook (v3 looped forever here)
|
|
if name == L.LIFETAP then
|
|
local _, _, r = string.find(rankText or "", "(%d+)")
|
|
r = tonumber(r) or 1
|
|
if BASE[r] then
|
|
spellIndex[r] = i
|
|
if r > maxRank then maxRank = r end
|
|
end
|
|
end
|
|
i = i + 1
|
|
end
|
|
end
|
|
|
|
-- ID scan (ClassicAPI): finds each known rank by spell ID. Takes precedence over
|
|
-- the name scan for the ranks it finds, so it also works when the name or rank
|
|
-- text doesn't match the locale tables.
|
|
local function scanSpellbookByID()
|
|
if type(FindSpellBookSlotByID) ~= "function" then return end
|
|
for r = 1, NUM_RANKS do
|
|
local ok, slot, book = pcall(FindSpellBookSlotByID, RANK_SPELL_IDS[r])
|
|
if ok and tonumber(slot) and (book == nil or book == BOOKTYPE_SPELL) then
|
|
spellIndex[r] = tonumber(slot)
|
|
if r > maxRank then maxRank = r end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function scanSpellbook()
|
|
for k in pairs(spellIndex) do spellIndex[k] = nil end
|
|
maxRank = 0
|
|
scanSpellbookByName()
|
|
scanSpellbookByID()
|
|
spellsDirty = false
|
|
end
|
|
|
|
local function scanTalents()
|
|
talentMult = 1
|
|
for tab = 1, (GetNumTalentTabs() or 0) do
|
|
for t = 1, (GetNumTalents(tab) or 0) do
|
|
local name, _, _, _, rank = GetTalentInfo(tab, t)
|
|
if name == L.IMP_LIFETAP then
|
|
talentMult = 1 + 0.1 * (rank or 0)
|
|
talentDirty = false
|
|
return
|
|
end
|
|
end
|
|
end
|
|
talentDirty = false
|
|
end
|
|
|
|
---------------------------------------------------------------------------
|
|
-- base health check against the game's own spell data (ClassicAPI)
|
|
-- C_Spell.GetSpellDescription gives the base-rank description text, e.g. the
|
|
-- "Converts N health into N mana" line, so the first number in it is the base
|
|
-- health of that rank. The check only reports; /slt verify adopt applies it.
|
|
---------------------------------------------------------------------------
|
|
local descCache, descText = {}, {}
|
|
|
|
local function hasDescription()
|
|
return type(C_Spell) == "table" and type(C_Spell.GetSpellDescription) == "function"
|
|
end
|
|
|
|
local function descBase(r)
|
|
if descCache[r] ~= nil then return descCache[r] or nil, descText[r] end
|
|
if not hasDescription() then return nil end
|
|
local ok, text = pcall(C_Spell.GetSpellDescription, RANK_SPELL_IDS[r])
|
|
if not ok or type(text) ~= "string" or text == "" then return nil end
|
|
text = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
|
|
text = string.gsub(text, "|r", "")
|
|
local _, _, n = string.find(text, "(%d+)")
|
|
n = tonumber(n)
|
|
descCache[r] = n or false
|
|
descText[r] = text
|
|
return n, text
|
|
end
|
|
|
|
local function verifyBase(adopt)
|
|
if not hasDescription() then
|
|
msg("Checking the numbers needs ClassicAPI (C_Spell.GetSpellDescription).")
|
|
return
|
|
end
|
|
local diffs, readable = 0, 0
|
|
for r = 1, NUM_RANKS do
|
|
local n, text = descBase(r)
|
|
local used = baseOf(r)
|
|
if not n then
|
|
msg(string.format("Rank %d: no readable description", r))
|
|
else
|
|
readable = readable + 1
|
|
if n == used then
|
|
msg(string.format("Rank %d: %d health - OK", r, n))
|
|
else
|
|
diffs = diffs + 1
|
|
msg(string.format("Rank %d: addon uses %d, spell data says %d (%s)", r, used, n, string.sub(text, 1, 80)))
|
|
if adopt then
|
|
if n >= BASE[r] * 0.5 and n <= BASE[r] * 2 then
|
|
SLT_Config.baseOverride = SLT_Config.baseOverride or {}
|
|
SLT_Config.baseOverride[r] = n
|
|
msg(string.format(" rank %d now uses %d", r, n))
|
|
else
|
|
msg(string.format(" rank %d: %d looks implausible, not adopted", r, n))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
SLT_Config.verifyShown = true
|
|
if readable == 0 then
|
|
msg("No rank description could be read.")
|
|
elseif diffs == 0 then
|
|
msg("Built-in numbers match the spell data.")
|
|
elseif not adopt then
|
|
msg("/slt verify adopt uses the spell data numbers; /slt verify reset goes back to the built-in ones.")
|
|
end
|
|
end
|
|
|
|
-- Once per character: tell the player if the spell data disagrees with the table.
|
|
local autoVerified = false
|
|
local function autoVerify()
|
|
if autoVerified then return end
|
|
autoVerified = true
|
|
if SLT_Config.verifyShown or not hasDescription() then return end
|
|
local bad = 0
|
|
for r = 1, NUM_RANKS do
|
|
local n = descBase(r)
|
|
if n and n ~= baseOf(r) then bad = bad + 1 end
|
|
end
|
|
if bad > 0 then
|
|
SLT_Config.verifyShown = true
|
|
msg(string.format("The game's spell data disagrees with the built-in Life Tap numbers for %d rank(s). Type /slt verify for details.", bad))
|
|
end
|
|
end
|
|
|
|
---------------------------------------------------------------------------
|
|
-- spell damage
|
|
---------------------------------------------------------------------------
|
|
-- Each source returns (genericDamage, shadowDamage) or nil when unavailable.
|
|
-- ClassicAPI and Nampower read the client's own computed shadow spell damage,
|
|
-- which already includes gear, enchants, buffs and set bonuses.
|
|
local SOURCES = {
|
|
classicapi = function()
|
|
if type(GetSpellBonusDamage) ~= "function" then return nil end
|
|
local ok, v = pcall(GetSpellBonusDamage, 6) -- 6 = Shadow (1-based)
|
|
if ok and tonumber(v) then return 0, tonumber(v) end
|
|
end,
|
|
nampower = function()
|
|
if type(GetSpellPower) ~= "function" or type(GetNampowerVersion) ~= "function" then return nil end
|
|
local ok, _, _, _, _, _, shadow = pcall(GetSpellPower) -- 6th value = Shadow
|
|
if ok and tonumber(shadow) then return 0, tonumber(shadow) end
|
|
end,
|
|
bonusscanner = function()
|
|
if not (BonusScanner and BonusScanner.GetBonus and IsAddOnLoaded("BonusScanner")) then return nil end
|
|
return tonumber(BonusScanner:GetBonus("DMG")) or 0, tonumber(BonusScanner:GetBonus("SHADOWDMG")) or 0
|
|
end,
|
|
internal = function() return Scanner:Get() end,
|
|
}
|
|
local SOURCE_NAMES = { classicapi = "ClassicAPI", nampower = "Nampower", bonusscanner = "BonusScanner", internal = "built-in scanner" }
|
|
-- BonusScanner is deliberately not in the auto order: it gives the same totals
|
|
-- as the built-in scanner on English clients and likely 0 on German/French ones.
|
|
-- It can still be forced with /slt source bonusscanner.
|
|
local AUTO_ORDER = { "classicapi", "nampower", "internal" }
|
|
local LIVE_SOURCES = { classicapi = true, nampower = true } -- these already include buffs
|
|
|
|
function ScaledLifeTap_GetDamageBonus()
|
|
local want = SLT_Config.source or "auto"
|
|
local dmg, shadow, key
|
|
if want ~= "auto" and SOURCES[want] then
|
|
dmg, shadow = SOURCES[want]()
|
|
key = want
|
|
end
|
|
if not dmg then
|
|
for i = 1, table.getn(AUTO_ORDER) do
|
|
key = AUTO_ORDER[i]
|
|
dmg, shadow = SOURCES[key]()
|
|
if dmg then break end
|
|
end
|
|
end
|
|
local total = (dmg or 0) + (shadow or 0) + (SLT_Config.bonus or 0)
|
|
return total, dmg or 0, shadow or 0, SOURCE_NAMES[key], LIVE_SOURCES[key]
|
|
end
|
|
|
|
---------------------------------------------------------------------------
|
|
-- mana check: compares predicted mana per Life Tap with what the server
|
|
-- actually gave, and learns a correction factor if your server's numbers
|
|
-- differ from standard 1.12. The actual amount comes from Nampower's
|
|
-- SPELL_ENERGIZE_BY_SELF event or ClassicAPI's combat log (SPELL_ENERGIZE).
|
|
---------------------------------------------------------------------------
|
|
local lastCast -- { rank, predicted, time }
|
|
local warned = false
|
|
|
|
local combatLog -- frame listening to ClassicAPI's combat log, only while a tap is pending
|
|
|
|
local function stopCombatLog()
|
|
if combatLog then combatLog:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED") end
|
|
end
|
|
|
|
local function onEnergize(spellId, powerType, amount, nameChecked)
|
|
if not lastCast or powerType ~= 0 or not tonumber(amount) then return end
|
|
if GetTime() - lastCast.time > 3 then lastCast = nil; stopCombatLog(); return end
|
|
if not nameChecked then
|
|
if not isLifeTapId(spellId) and type(GetSpellNameAndRankForId) == "function" then
|
|
local ok, name = pcall(GetSpellNameAndRankForId, spellId)
|
|
if not ok or name ~= L.LIFETAP then return end
|
|
end
|
|
end
|
|
local base = lastCast.predicted / (SLT_Config.gainFactor or 1)
|
|
lastCast = nil
|
|
stopCombatLog()
|
|
if base <= 0 or not SLT_Config.calibrate then return end
|
|
local ratio = amount / base
|
|
if ratio < 0.5 or ratio > 2 then return end -- ignore nonsense
|
|
-- smooth toward the observed ratio
|
|
SLT_Config.gainFactor = (SLT_Config.gainFactor or 1) * 0.7 + ratio * 0.3
|
|
SLT_Config.samples = (SLT_Config.samples or 0) + 1
|
|
if not warned and SLT_Config.samples >= 3 and math.abs(SLT_Config.gainFactor - 1) > 0.03 then
|
|
warned = true
|
|
msg(string.format("Life Tap is giving %d%% of the predicted mana on this server; adjusting automatically (/slt calibrate reset to undo).",
|
|
SLT_Config.gainFactor * 100 + 0.5))
|
|
end
|
|
end
|
|
|
|
-- ClassicAPI combat log. COMBAT_LOG_EVENT_UNFILTERED fires for everything
|
|
-- near you, so the frame registers it only after a tap and drops it as soon
|
|
-- as the energize arrives (or 3 seconds pass).
|
|
local function hasCombatLog()
|
|
return type(CombatLogGetCurrentEventInfo) == "function" and type(UnitGUID) == "function"
|
|
end
|
|
|
|
local function onCombatLog()
|
|
local _, sub, _, src, _, _, _, _, _, _, _, spellId, spellName, _, amount, a16, a17 = CombatLogGetCurrentEventInfo()
|
|
if sub ~= "SPELL_ENERGIZE" then return end
|
|
if not (isLifeTapId(spellId) or spellName == L.LIFETAP) then return end
|
|
if src ~= UnitGUID("player") then return end
|
|
-- raw values after the spell fields, to confirm which one is the power type
|
|
dbg(string.format("energize: amount=%s next=%s next2=%s", tostring(amount), tostring(a16), tostring(a17)))
|
|
onEnergize(spellId, a16, amount, true)
|
|
end
|
|
|
|
local function watchCombatLog()
|
|
if not hasCombatLog() then return end
|
|
if not combatLog then
|
|
combatLog = CreateFrame("Frame")
|
|
combatLog:SetScript("OnEvent", onCombatLog)
|
|
end
|
|
combatLog:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
|
|
if C_Timer and C_Timer.After then C_Timer.After(3.5, stopCombatLog) end
|
|
end
|
|
|
|
---------------------------------------------------------------------------
|
|
-- rank selection
|
|
---------------------------------------------------------------------------
|
|
-- returns rank (or nil), hpCost, manaGain
|
|
function ScaledLifeTap_ChooseRank()
|
|
if spellsDirty then scanSpellbook() end
|
|
if talentDirty then scanTalents() end
|
|
if maxRank == 0 then return nil end
|
|
|
|
local bonus = ScaledLifeTap_GetDamageBonus()
|
|
local mult = talentMult * (SLT_Config.gainFactor or 1)
|
|
local hp = UnitHealth("player")
|
|
local missing = UnitManaMax("player") - UnitMana("player")
|
|
local floor = UnitHealthMax("player") * (SLT_Config.reserve or 0) / 100
|
|
|
|
for r = maxRank, 1, -1 do
|
|
if spellIndex[r] then
|
|
local cost = baseOf(r) + COEF[r] * bonus
|
|
local gain = cost * mult
|
|
if hp - cost > floor and missing >= gain then return r, cost, gain, true end
|
|
end
|
|
end
|
|
|
|
if SLT_Config.overtap and missing > 0 then
|
|
local r = 1
|
|
while r <= maxRank and not spellIndex[r] do r = r + 1 end
|
|
if spellIndex[r] then
|
|
local cost = baseOf(r) + COEF[r] * bonus
|
|
if hp - cost > floor then return r, cost, cost * mult, false end
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function ScaledLifeTap_CastLifeTap()
|
|
local r, cost, gain, fits = ScaledLifeTap_ChooseRank()
|
|
if r then
|
|
CastSpell(spellIndex[r], BOOKTYPE_SPELL)
|
|
-- only fully-fitting casts are used for the mana check (overtap casts may hit the mana cap)
|
|
if fits and SLT_Config.calibrate then
|
|
lastCast = { rank = r, predicted = gain, time = GetTime() }
|
|
watchCombatLog()
|
|
else
|
|
lastCast = nil
|
|
end
|
|
dbg(string.format("Rank %d: -%d hp, +%d mana", r, cost, gain))
|
|
elseif maxRank == 0 then
|
|
dbg("Life Tap not found in spellbook.")
|
|
else
|
|
dbg("No rank fits (mana full or health too low).")
|
|
end
|
|
end
|
|
|
|
-- backwards compatibility with v3 macros
|
|
ScaledLifetap_CastLifeTap = ScaledLifeTap_CastLifeTap
|
|
function ScaledLifeTap_Debug(arg)
|
|
if arg == nil then return SLT_Config.debug end
|
|
SLT_Config.debug = (arg == 1 or arg == true)
|
|
end
|
|
|
|
---------------------------------------------------------------------------
|
|
-- macro button display (ClassicAPI): macros that contain "/slt cast" show the
|
|
-- Life Tap rank that would be cast right now (icon and tooltip), the same way
|
|
-- #showtooltip follows a /cast line. Updates are throttled and event driven.
|
|
---------------------------------------------------------------------------
|
|
local macroSlots = {} -- macro indexes whose body calls /slt cast
|
|
local macroDirty = true
|
|
local lastShown -- rank last published, so unchanged ranks cost nothing
|
|
local displayPending = false
|
|
local displayBroken = false
|
|
|
|
local function hasMacroDisplay()
|
|
return type(C_Macro) == "table" and type(C_Macro.SetMacroDisplay) == "function"
|
|
end
|
|
|
|
local function findMacros()
|
|
for k in pairs(macroSlots) do macroSlots[k] = nil end
|
|
if type(GetMacroInfo) == "function" then
|
|
for i = 1, 36 do
|
|
local name, _, body = GetMacroInfo(i)
|
|
if name and body then
|
|
local b = string.lower(body)
|
|
if string.find(b, "/slt cast", 1, true)
|
|
or string.find(b, "/scaledlifetap cast", 1, true)
|
|
or string.find(b, "scaledlifetap_castlifetap", 1, true) then
|
|
table.insert(macroSlots, i)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
macroDirty = false
|
|
lastShown = nil
|
|
end
|
|
|
|
-- Tries the spell ID first, then "Life Tap(Rank N)" built from the spellbook entry.
|
|
local function setMacroDisplay(slot, r)
|
|
local ok, res = pcall(C_Macro.SetMacroDisplay, slot, RANK_SPELL_IDS[r])
|
|
if ok and res ~= false then return true end
|
|
if spellIndex[r] then
|
|
local name, rankText = GetSpellName(spellIndex[r], BOOKTYPE_SPELL)
|
|
if name then
|
|
local value = name
|
|
if rankText and rankText ~= "" then value = name .. "(" .. rankText .. ")" end
|
|
ok, res = pcall(C_Macro.SetMacroDisplay, slot, value)
|
|
if ok and res ~= false then return true end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function publishDisplay()
|
|
displayPending = false
|
|
if displayBroken or not SLT_Config.macroDisplay or not hasMacroDisplay() then return end
|
|
if macroDirty then findMacros() end
|
|
if table.getn(macroSlots) == 0 then return end
|
|
if spellsDirty then scanSpellbook() end
|
|
if maxRank == 0 then return end
|
|
local r = ScaledLifeTap_ChooseRank()
|
|
if not r then r = maxRank end -- nothing fits (mana full): show the top rank
|
|
if r == lastShown then return end
|
|
local failed = 0
|
|
for i = 1, table.getn(macroSlots) do
|
|
if not setMacroDisplay(macroSlots[i], r) then failed = failed + 1 end
|
|
end
|
|
if failed == table.getn(macroSlots) then
|
|
displayBroken = true
|
|
msg("ClassicAPI did not accept the macro display value; macro button display is off for this session. (/slt macrodisplay off to hide this)")
|
|
else
|
|
lastShown = r
|
|
dbg(string.format("macro display: rank %d on %d macro(s)", r, table.getn(macroSlots) - failed))
|
|
end
|
|
end
|
|
|
|
local function requestDisplay()
|
|
if displayPending or displayBroken or not SLT_Config.macroDisplay or not hasMacroDisplay() then return end
|
|
if not (C_Timer and C_Timer.After) then return end
|
|
displayPending = true
|
|
C_Timer.After(0.3, publishDisplay)
|
|
end
|
|
|
|
---------------------------------------------------------------------------
|
|
-- status / slash commands
|
|
---------------------------------------------------------------------------
|
|
local function onoff(v) return v and "|cff00ff00on|r" or "|cffff0000off|r" end
|
|
|
|
-- Status as plain lines, shared by /slt status and the options page.
|
|
-- summary: spell damage line, multiplier line. ranks: one line per known rank.
|
|
local function statusLines()
|
|
if spellsDirty then scanSpellbook() end
|
|
if talentDirty then scanTalents() end
|
|
local total, dmg, shadow, source, live = ScaledLifeTap_GetDamageBonus()
|
|
local summary, ranks = {}, {}
|
|
if live then
|
|
summary[1] = string.format("Spell damage: %d shadow (incl. buffs) + %d manual = %d [%s]", shadow, SLT_Config.bonus or 0, total, source)
|
|
else
|
|
summary[1] = string.format("Spell damage: %d + %d shadow + %d manual = %d [%s, gear only]", dmg, shadow, SLT_Config.bonus or 0, total, source)
|
|
end
|
|
local mult = talentMult * (SLT_Config.gainFactor or 1)
|
|
summary[2] = string.format("Improved Life Tap x%.1f, server correction x%.3f (%d samples)",
|
|
talentMult, SLT_Config.gainFactor or 1, SLT_Config.samples or 0)
|
|
if maxRank == 0 then
|
|
ranks[1] = "Life Tap is not in your spellbook."
|
|
end
|
|
for r = maxRank, 1, -1 do
|
|
if spellIndex[r] then
|
|
local cost = baseOf(r) + COEF[r] * total
|
|
table.insert(ranks, string.format("Rank %d: -%d health, +%d mana", r, cost, cost * mult))
|
|
end
|
|
end
|
|
return summary, ranks
|
|
end
|
|
|
|
local function printStatus()
|
|
local summary, ranks = statusLines()
|
|
for i = 1, table.getn(summary) do msg(summary[i]) end
|
|
msg(string.format("overtap %s | reserve %d%% | mana check %s | macro display %s", onoff(SLT_Config.overtap), SLT_Config.reserve or 0, onoff(SLT_Config.calibrate), onoff(SLT_Config.macroDisplay)))
|
|
for i = 1, table.getn(ranks) do msg(" " .. ranks[i]) end
|
|
end
|
|
|
|
---------------------------------------------------------------------------
|
|
-- small API for Options.lua
|
|
---------------------------------------------------------------------------
|
|
ScaledLifeTap = {
|
|
DEFAULTS = DEFAULTS,
|
|
StatusLines = statusLines,
|
|
SourceNames = SOURCE_NAMES,
|
|
IsSourceAvailable = function(key)
|
|
if key == "auto" or key == "internal" then return true end
|
|
return SOURCES[key] and SOURCES[key]() ~= nil
|
|
end,
|
|
IsLiveSource = function()
|
|
local _, _, _, _, live = ScaledLifeTap_GetDamageBonus()
|
|
return live and true or false
|
|
end,
|
|
CanCalibrate = function()
|
|
return type(GetNampowerVersion) == "function" or hasCombatLog()
|
|
end,
|
|
CanMacroDisplay = hasMacroDisplay,
|
|
RefreshMacroDisplay = function()
|
|
lastShown = nil
|
|
macroDirty = true
|
|
displayBroken = false
|
|
requestDisplay()
|
|
end,
|
|
ResetCalibration = function()
|
|
SLT_Config.gainFactor, SLT_Config.samples = 1, 0
|
|
warned = false
|
|
end,
|
|
Rescan = function()
|
|
Scanner:MarkDirty(); spellsDirty = true; talentDirty = true
|
|
lastShown = nil
|
|
requestDisplay()
|
|
end,
|
|
}
|
|
|
|
local function toggleArg(arg, current)
|
|
if arg == "on" then return true elseif arg == "off" then return false end
|
|
return not current
|
|
end
|
|
|
|
SLASH_SCALEDLIFETAP1 = "/slt"
|
|
SLASH_SCALEDLIFETAP2 = "/scaledlifetap"
|
|
SlashCmdList["SCALEDLIFETAP"] = function(input)
|
|
local _, _, cmd, arg = string.find(string.lower(input or ""), "^%s*(%S*)%s*(.-)%s*$")
|
|
if cmd == "cast" then
|
|
ScaledLifeTap_CastLifeTap()
|
|
elseif cmd == "status" then
|
|
printStatus()
|
|
elseif cmd == "overtap" then
|
|
SLT_Config.overtap = toggleArg(arg, SLT_Config.overtap); msg("overtap " .. onoff(SLT_Config.overtap))
|
|
elseif cmd == "debug" then
|
|
SLT_Config.debug = toggleArg(arg, SLT_Config.debug); msg("debug " .. onoff(SLT_Config.debug))
|
|
elseif cmd == "reserve" and tonumber(arg) then
|
|
local v = math.max(0, math.min(90, tonumber(arg)))
|
|
SLT_Config.reserve = v; msg("health reserve " .. v .. "%")
|
|
elseif cmd == "bonus" and tonumber(arg) then
|
|
SLT_Config.bonus = tonumber(arg); msg("manual spell damage bonus " .. SLT_Config.bonus)
|
|
elseif cmd == "calibrate" and (arg == "on" or arg == "off" or arg == "") then
|
|
SLT_Config.calibrate = toggleArg(arg, SLT_Config.calibrate); msg("mana check " .. onoff(SLT_Config.calibrate))
|
|
elseif cmd == "calibrate" and arg == "reset" then
|
|
ScaledLifeTap.ResetCalibration(); msg("mana correction reset")
|
|
elseif cmd == "source" and (arg == "auto" or SOURCES[arg]) then
|
|
SLT_Config.source = arg; Scanner:MarkDirty(); msg("damage source " .. arg)
|
|
elseif cmd == "verify" and (arg == "" or arg == "adopt") then
|
|
verifyBase(arg == "adopt")
|
|
if arg == "adopt" then lastShown = nil; requestDisplay() end
|
|
elseif cmd == "verify" and arg == "reset" then
|
|
SLT_Config.baseOverride = nil; lastShown = nil; msg("Life Tap base numbers reset to the built-in table"); requestDisplay()
|
|
elseif cmd == "macrodisplay" and (arg == "on" or arg == "off" or arg == "") then
|
|
SLT_Config.macroDisplay = toggleArg(arg, SLT_Config.macroDisplay)
|
|
msg("macro button display " .. onoff(SLT_Config.macroDisplay))
|
|
if SLT_Config.macroDisplay then ScaledLifeTap.RefreshMacroDisplay() end
|
|
if not hasMacroDisplay() then msg("(needs ClassicAPI; nothing happens without it)") end
|
|
elseif cmd == "rescan" then
|
|
ScaledLifeTap.Rescan(); printStatus()
|
|
elseif cmd == "options" or cmd == "config" then
|
|
if ScaledLifeTap.OpenOptions then ScaledLifeTap.OpenOptions() end
|
|
else
|
|
msg("commands: (or open the options window with /slt options)")
|
|
msg(" /slt cast - cast the best Life Tap rank (put this in a macro)")
|
|
msg(" /slt status - show bonus, talent and per-rank numbers")
|
|
msg(" /slt overtap [on|off] - fall back to rank 1 when no rank fits (" .. onoff(SLT_Config.overtap) .. ")")
|
|
msg(" /slt reserve <0-90> - never tap below this % health (" .. (SLT_Config.reserve or 0) .. "%)")
|
|
msg(" /slt bonus <n> - extra spell damage to add, e.g. from elixirs (" .. (SLT_Config.bonus or 0) .. ")")
|
|
msg(" /slt source auto|classicapi|nampower|bonusscanner|internal - where spell damage comes from (" .. SLT_Config.source .. ")")
|
|
msg(" /slt calibrate [on|off|reset] - learn actual mana per tap (needs Nampower or ClassicAPI) (" .. onoff(SLT_Config.calibrate) .. ")")
|
|
msg(" /slt verify [adopt|reset] - compare the base health numbers with the game's spell data (needs ClassicAPI)")
|
|
msg(" /slt macrodisplay [on|off] - show the rank on /slt cast macro buttons (needs ClassicAPI) (" .. onoff(SLT_Config.macroDisplay) .. ")")
|
|
msg(" /slt debug [on|off] - print each cast (" .. onoff(SLT_Config.debug) .. ")")
|
|
msg(" /slt rescan - force a gear/spellbook/talent rescan")
|
|
msg(" /slt options - open the options window")
|
|
end
|
|
end
|
|
|
|
---------------------------------------------------------------------------
|
|
-- events
|
|
---------------------------------------------------------------------------
|
|
SLT_Config = SLT_Config or {}
|
|
|
|
local f = CreateFrame("Frame", "ScaledLifeTapEventFrame")
|
|
f:RegisterEvent("VARIABLES_LOADED")
|
|
f:RegisterEvent("UNIT_INVENTORY_CHANGED")
|
|
f:RegisterEvent("SPELLS_CHANGED")
|
|
f:RegisterEvent("LEARNED_SPELL_IN_TAB")
|
|
f:RegisterEvent("CHARACTER_POINTS_CHANGED")
|
|
f:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
f:RegisterEvent("UPDATE_MACROS")
|
|
f:RegisterEvent("UNIT_MANA")
|
|
f:RegisterEvent("UNIT_MAXMANA")
|
|
f:RegisterEvent("UNIT_HEALTH")
|
|
f:RegisterEvent("UNIT_MAXHEALTH")
|
|
-- Nampower custom event (registering it enables it on Nampower 4.5+; older
|
|
-- versions need /run SetCVar("NP_EnableSpellEnergizeEvents","1") ). Harmless otherwise.
|
|
pcall(f.RegisterEvent, f, "SPELL_ENERGIZE_BY_SELF")
|
|
f:SetScript("OnEvent", function()
|
|
if event == "SPELL_ENERGIZE_BY_SELF" then
|
|
onEnergize(arg3, arg4, arg5)
|
|
elseif event == "UNIT_MANA" or event == "UNIT_MAXMANA" or event == "UNIT_HEALTH" or event == "UNIT_MAXHEALTH" then
|
|
if arg1 == "player" then requestDisplay() end
|
|
elseif event == "UNIT_INVENTORY_CHANGED" then
|
|
if arg1 == "player" then Scanner:MarkDirty(); requestDisplay() end
|
|
elseif event == "SPELLS_CHANGED" or event == "LEARNED_SPELL_IN_TAB" then
|
|
spellsDirty = true
|
|
requestDisplay()
|
|
elseif event == "CHARACTER_POINTS_CHANGED" then
|
|
talentDirty = true
|
|
requestDisplay()
|
|
elseif event == "UPDATE_MACROS" then
|
|
macroDirty = true
|
|
requestDisplay()
|
|
elseif event == "PLAYER_ENTERING_WORLD" then
|
|
Scanner:MarkDirty(); spellsDirty = true; talentDirty = true
|
|
macroDirty = true
|
|
autoVerify()
|
|
requestDisplay()
|
|
elseif event == "VARIABLES_LOADED" then
|
|
if type(SLT_Config) ~= "table" then SLT_Config = {} end
|
|
-- v3 stored "overtapEnabled"
|
|
if SLT_Config.overtapEnabled ~= nil and SLT_Config.overtap == nil then
|
|
SLT_Config.overtap = SLT_Config.overtapEnabled
|
|
end
|
|
SLT_Config.overtapEnabled = nil
|
|
for k, v in pairs(DEFAULTS) do
|
|
if SLT_Config[k] == nil then SLT_Config[k] = v end
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- make sure defaults exist even before VARIABLES_LOADED (e.g. when loaded late)
|
|
for k, v in pairs(DEFAULTS) do if SLT_Config[k] == nil then SLT_Config[k] = v end end
|