Files
MetaHunt/modules/chronometer/engine.lua
T
DuvelCorp 1a28d95b87 v1.0.5
2026-03-01 09:10:22 +01:00

1400 lines
39 KiB
Lua

local Chronometer = AceLibrary("AceAddon-2.0"):new("AceEvent-2.0", "AceHook-2.1", "CandyBar-2.0")
local BS = {
GetSpellIcon = function(self, spellName)
local cacheOwner = type(self) == "table" and self or {}
local name = tostring(spellName or "")
if name == "" then
return "Interface\\Icons\\INV_Misc_QuestionMark"
end
local iconCache = rawget(cacheOwner, "_iconCache")
if type(iconCache) ~= "table" then
iconCache = {}
rawset(cacheOwner, "_iconCache", iconCache)
end
if iconCache[name] then
return iconCache[name]
end
if type(GetSpellName) == "function" and type(GetSpellTexture) == "function" then
local spellBookType = BOOKTYPE_SPELL or "spell"
local i = 1
while true do
local okName, n = pcall(GetSpellName, i, spellBookType)
if not okName or not n then
break
end
if n == name then
local okTex, tex = pcall(GetSpellTexture, i, spellBookType)
if okTex and tex then
iconCache[name] = tex
return tex
end
break
end
i = i + 1
end
end
iconCache[name] = "Interface\\Icons\\INV_Misc_QuestionMark"
return "Interface\\Icons\\INV_Misc_QuestionMark"
end,
}
BS = setmetatable(BS, {
__index = function(_, key)
if MTH and MTH.LocalizeSpell then
return MTH:LocalizeSpell(key)
end
return key
end,
})
MTH_ChronometerHunter = Chronometer
Chronometer.SPELL = 1
Chronometer.EVENT = 2
Chronometer.rightclick = false
Chronometer.dataSetup = {}
local latins = { I = 1, II = 2, III = 3, IV = 4, V = 5, VI = 6, VII = 7, VIII = 8, IX = 9, X = 10, XI = 11, XII = 12, XIII = 13, XIV = 14 }
local BAR_GROUP = "MTHChronometer"
local PARSER_OWNER = "MTHChronometerHunter"
local COLOR_NAME_MAP = {
white = { 1.0, 1.0, 1.0 },
black = { 0.0, 0.0, 0.0 },
blue = { 0.0, 0.0, 1.0 },
magenta = { 1.0, 0.0, 1.0 },
cyan = { 0.0, 1.0, 1.0 },
green = { 0.0, 1.0, 0.0 },
yellow = { 1.0, 1.0, 0.0 },
orange = { 1.0, 0.5, 0.0 },
red = { 1.0, 0.0, 0.0 },
gray = { 0.5, 0.5, 0.5 },
forest = { 0.0, 0.5, 0.0 },
maroon = { 0.5, 0.0, 0.0 },
navy = { 0.0, 0.0, 0.5 },
olive = { 0.5, 0.5, 0.0 },
purple = { 0.5, 0.0, 0.5 },
teal = { 0.0, 0.5, 0.5 },
}
local DEFAULTS = {
growup = false,
reverse = false,
fadeonkill = true,
fadeonfade = true,
barposition = {},
ghost = 0,
selfbars = true,
barwidth = 220,
barheight = 16,
spacing = 0,
barscale = 1,
iconposition = "LEFT",
textsize = 10,
textcolor = "white",
bgcolor = "teal",
barcolor = "gray",
bordercolor = "black",
bordertex = "None",
bgalpha = 0.5,
text = "$t",
onlyself = false,
disabledSpells = {
COMMON = {
["Ephemeral Power"] = {},
["Mind Quickening"] = {},
["Unstable Power"] = {},
},
RACIAL = {},
HUNTER = {},
},
}
local function seedDefaultCommonDisabled(commonBucket)
if type(commonBucket) ~= "table" then
return
end
local names = {
"Ephemeral Power",
"Mind Quickening",
"Unstable Power",
}
for i = 1, table.getn(names) do
local baseName = names[i]
if commonBucket[baseName] == nil then
commonBucket[baseName] = {}
end
if MTH and MTH.LocalizeSpell then
local localized = MTH:LocalizeSpell(baseName)
if localized and localized ~= "" and commonBucket[localized] == nil then
commonBucket[localized] = {}
end
end
end
end
local function shallowCopy(source)
local dest = {}
for key, value in pairs(source) do
dest[key] = value
end
return dest
end
local function deepCopyValue(value)
if type(value) ~= "table" then
return value
end
local copy = {}
for key, entry in pairs(value) do
copy[key] = deepCopyValue(entry)
end
return copy
end
local function ensureProfile()
if not MTH or not MTH.GetModuleCharSavedVariables then
return shallowCopy(DEFAULTS)
end
local store = MTH:GetModuleCharSavedVariables("chronometer")
if type(store) ~= "table" then
return shallowCopy(DEFAULTS)
end
if type(store.profile) ~= "table" and MTH.GetModuleSavedVariables then
local accountStore = MTH:GetModuleSavedVariables("chronometer")
if type(accountStore) == "table" and type(accountStore.profile) == "table" then
store.profile = deepCopyValue(accountStore.profile)
end
end
if type(store.profile) ~= "table" then
store.profile = {}
end
local profile = store.profile
for key, value in pairs(DEFAULTS) do
if profile[key] == nil then
if type(value) == "table" then
profile[key] = shallowCopy(value)
else
profile[key] = value
end
end
end
if type(profile.disabledSpells) ~= "table" then
profile.disabledSpells = {}
end
if type(profile.disabledSpells.COMMON) ~= "table" then
profile.disabledSpells.COMMON = {}
end
seedDefaultCommonDisabled(profile.disabledSpells.COMMON)
if type(profile.disabledSpells.RACIAL) ~= "table" then
profile.disabledSpells.RACIAL = {}
end
if type(profile.disabledSpells.HUNTER) ~= "table" then
profile.disabledSpells.HUNTER = {}
end
return profile
end
local function MTH_CHRON_IsPersistedEnabled()
if MTH and MTH.IsModuleEnabled then
return MTH:IsModuleEnabled("chronometer", true) and true or false
end
if type(MTH_SavedVariables) == "table" then
if type(MTH_SavedVariables.modules) == "table"
and type(MTH_SavedVariables.modules.chronometer) == "table"
and MTH_SavedVariables.modules.chronometer.enabled ~= nil
then
return MTH_SavedVariables.modules.chronometer.enabled and true or false
end
if type(MTH_SavedVariables.chronometer) == "table"
and MTH_SavedVariables.chronometer.enabled ~= nil
then
return MTH_SavedVariables.chronometer.enabled and true or false
end
end
return true
end
local function convertcolor(color)
if type(color) == "string" then
local key = string.lower(color)
local named = COLOR_NAME_MAP[key]
if named then
return { named[1], named[2], named[3] }
end
if string.len(key) == 6 then
local rr = tonumber(string.sub(key, 1, 2), 16)
local gg = tonumber(string.sub(key, 3, 4), 16)
local bb = tonumber(string.sub(key, 5, 6), 16)
if rr and gg and bb then
return { rr / 255, gg / 255, bb / 255 }
end
end
return { 1, 1, 1 }
elseif type(color) == "table" then
if type(color[1]) == "table" then
color = color[1]
end
local r = tonumber(color[1] or color.r)
local g = tonumber(color[2] or color.g)
local b = tonumber(color[3] or color.b)
if r and g and b then
return { r, g, b }
end
return { 1, 1, 1 }
else
return { 1, 1, 1 }
end
end
local function parseRankNumber(text)
if not text or text == "" then
return 0
end
local _, _, rankText = string.find(tostring(text), "(%d+)")
local rank = tonumber(rankText)
if rank then
return rank
end
local _, _, roman = string.find(tostring(text), "([IVX]+)")
if roman and latins[roman] then
return latins[roman]
end
return 0
end
local function getActionSpell(slot)
if GetActionText(slot) or not HasAction(slot) then
return nil, 0
end
if not Chronometer._scanTooltip then
Chronometer._scanTooltip = CreateFrame("GameTooltip", "MTH_ChronometerScanTooltip", UIParent, "GameTooltipTemplate")
Chronometer._scanTooltip:SetOwner(UIParent, "ANCHOR_NONE")
end
local tooltip = Chronometer._scanTooltip
tooltip:ClearLines()
tooltip:SetAction(slot)
local left1 = getglobal("MTH_ChronometerScanTooltipTextLeft1")
local left2 = getglobal("MTH_ChronometerScanTooltipTextLeft2")
local spellName = left1 and left1:GetText() or nil
if not spellName or spellName == "" then
return nil, 0
end
local rankText = left2 and left2:GetText() or ""
return spellName, parseRankNumber(rankText)
end
local function callGlobal(name, a1, a2, a3, a4)
if type(name) ~= "string" or name == "" then
return nil
end
local fn = getglobal(name)
if type(fn) == "function" then
return fn(a1, a2, a3, a4)
end
return nil
end
function Chronometer:MTH_Initialize()
if self._mth_initialized then
return
end
self.profile = ensureProfile()
self.parser = ParserLib:GetInstance("1.1")
self.COLOR_MAP = {
[0] = "olive",
[1] = "teal",
[2] = "purple",
[3] = "forest",
}
self.anchor = self:CreateAnchor("MetaHunt Chronometer", 0, 1, 0)
self:RegisterCandyBarGroup(BAR_GROUP)
self:SetCandyBarGroupPoint(BAR_GROUP, "TOP", self.anchor, "BOTTOM", 0, 0)
self._mth_initialized = true
end
function Chronometer:MTH_Enable()
self._mth_explicitEnable = true
self:MTH_Initialize()
self:OnEnable()
self._mth_explicitEnable = nil
end
function Chronometer:MTH_Disable()
self:OnDisable()
end
function Chronometer:OnEnable()
if self._mth_enabled then
return
end
if (not self._mth_explicitEnable) and (not MTH_CHRON_IsPersistedEnabled()) then
if self.parser then
self.parser:UnregisterAllEvents(PARSER_OWNER)
end
self:UnregisterAllEvents()
if self.CancelAllScheduledEvents then
self:CancelAllScheduledEvents()
end
self:UnhookAll()
if self.anchor then
self.anchor:Hide()
end
self.bars = nil
self.timers = nil
self.groups = nil
self.captive = nil
self.active = nil
self._mth_enabled = false
return
end
self:MTH_Initialize()
self._mth_enabled = true
self.profile = ensureProfile()
self:SetCandyBarGroupGrowth(BAR_GROUP, self.profile.growup and true or false)
self:SetCandyBarGroupVerticalSpacing(BAR_GROUP, self.profile.spacing or 0)
self.groups = {}
self.timers = {}
self.bars = {}
for i = 1, 20 do
self.bars[i] = {}
end
for _, setupFunc in pairs(self.dataSetup) do
setupFunc(self)
end
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_COMBAT_FRIENDLY_DEATH", function(event, info) self:COMBAT_DEATH(event, info) end)
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_COMBAT_HOSTILE_DEATH", function(event, info) self:COMBAT_DEATH(event, info) end)
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_COMBAT_XP_GAIN", function(event, info) self:COMBAT_DEATH(event, info) end)
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_SPELL_AURA_GONE_SELF", function(event, info) self:SPELL_FADE(event, info) end)
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_SPELL_AURA_GONE_OTHER", function(event, info) self:SPELL_FADE(event, info) end)
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE", function(event, info) self:SPELL_PERIODIC(event, info) end)
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_SPELL_PERIODIC_CREATURE_BUFFS", function(event, info) self:SPELL_PERIODIC(event, info) end)
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_BUFFS", function(event, info) self:SPELL_PERIODIC(event, info) end)
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE", function(event, info) self:SPELL_PERIODIC(event, info) end)
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_SPELL_PERIODIC_SELF_BUFFS", function(event, info) self:SPELL_PERIODIC(event, info) end)
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE", function(event, info) self:SPELL_PERIODIC(event, info) end)
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS", function(event, info) self:SPELL_PERIODIC(event, info) end)
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_DAMAGE", function(event, info) self:SPELL_PERIODIC(event, info) end)
local enableRoM = false
for _, timer in pairs(self.timers[self.SPELL] or {}) do
if timer.x.rom or timer.x.romc then
enableRoM = true
break
end
end
if not enableRoM then
for _, timer in pairs(self.timers[self.EVENT] or {}) do
if timer.x.rom or timer.x.romc then
enableRoM = true
break
end
end
end
if enableRoM then
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_COMBAT_SELF_HITS", function(event, info) self:SELF_HITS(event, info) end)
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_COMBAT_SELF_CRITS", function(event, info) self:SELF_CRITS(event, info) end)
end
self.captive = {}
self.active = {}
if type(getglobal("UseAction")) == "function" then
self:Hook("UseAction")
end
if type(getglobal("CastSpell")) == "function" then
self:Hook("CastSpell")
end
if type(getglobal("CastSpellByName")) == "function" then
self:Hook("CastSpellByName")
end
if type(getglobal("SpellTargetUnit")) == "function" then
self:Hook("SpellTargetUnit")
end
if type(getglobal("TargetUnit")) == "function" then
self:Hook("TargetUnit")
end
if type(getglobal("SpellStopTargeting")) == "function" then
self:Hook("SpellStopTargeting")
end
if type(getglobal("SpellStopCasting")) == "function" then
self:Hook("SpellStopCasting")
end
if WorldFrame then
self:HookScript(WorldFrame, "OnMouseDown")
end
self:RegisterEvent("SPELLCAST_INTERRUPTED")
self:RegisterEvent("SPELLCAST_START")
self:RegisterEvent("SPELLCAST_STOP")
self:RegisterEvent("PLAYER_DEAD")
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_SPELL_SELF_DAMAGE", function(event, info) self:SELF_DAMAGE(event, info) end)
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_SPELL_DAMAGESHIELDS_ON_SELF", function(event, info) self:SELF_DAMAGE(event, info) end)
self.parser:RegisterEvent(PARSER_OWNER, "CHAT_MSG_SPELL_FAILED_LOCALPLAYER", function(event, info) self:SPELL_FAILED(event, info) end)
end
function Chronometer:OnDisable()
if not self._mth_enabled then
return
end
self._mth_enabled = false
if self.bars then
for i = 1, 20 do
if self.bars[i].id then
self:StopCandyBar(self.bars[i].id)
end
end
end
if self.anchor then
self.anchor:Hide()
end
self.bars = nil
self.timers = nil
self.groups = nil
self.captive = nil
self.active = nil
if self.parser then
self.parser:UnregisterAllEvents(PARSER_OWNER)
end
self:UnregisterAllEvents()
if self.CancelAllScheduledEvents then
self:CancelAllScheduledEvents()
end
self:UnhookAll()
end
function Chronometer:AddGroup(id, forall, color)
if color then
self.groups[id] = { fa = forall, cr = color }
else
self.groups[id] = { fa = forall }
end
end
function Chronometer:AddTimer(kind, name, duration, targeted, isgain, selforselect, extra)
if not self.timers[kind] then
self.timers[kind] = {}
end
if not self.timers[kind][name] then
self.timers[kind][name] = {}
end
if not extra then
extra = {}
end
targeted = (targeted and targeted > 0) and 1 or nil
isgain = (isgain and isgain > 0) and 1 or nil
selforselect = (selforselect and selforselect > 0) and 1 or nil
if not extra.cr then
if extra.gr and self.groups[extra.gr] and self.groups[extra.gr].cr then
extra.cr = self.groups[extra.gr].cr
else
local ccode = (targeted and 2 or 0) + (isgain and 1 or 0)
extra.cr = self.COLOR_MAP[ccode]
end
end
self.timers[kind][name] = { d = duration, k = { t = targeted, g = isgain, s = selforselect }, x = extra }
end
function Chronometer:StartTimer(timer, name, target, rank, durmod)
target = self:DisambiguateTarget(timer, target)
local idTarget = target
do
local sp = (BS and BS["Scorpid Poison"]) or "Scorpid Poison"
if name == sp and target and target ~= "none" then
local label = tostring(target)
local base = label
local pHash = string.find(label, " #", 1, true)
if pHash then
base = string.sub(base, 1, pHash - 1)
end
local pRt = string.find(base, " {RT", 1, true)
if pRt then
base = string.sub(base, 1, pRt - 1)
end
local guid = self:SPGetGUIDForName(base)
if guid then
self._spGuidOrdinal = self._spGuidOrdinal or {}
self._spGuidOrdinal[base] = self._spGuidOrdinal[base] or {}
local ord = self._spGuidOrdinal[base][guid]
if not ord then
local used = {}
for _, orderValue in pairs(self._spGuidOrdinal[base]) do
if type(orderValue) == "number" then
used[orderValue] = true
end
end
local n = 1
while used[n] do
n = n + 1
end
ord = n
self._spGuidOrdinal[base][guid] = ord
end
idTarget = base .. " #" .. guid
target = base .. " #" .. ord
end
end
end
local _, class = UnitClass("player")
local timerClass = timer.x.cl == nil and class or timer.x.cl
if self.profile.disabledSpells[timerClass] and self.profile.disabledSpells[timerClass][name] ~= nil then
return
end
if not target then
target = "none"
end
if not rank then
rank = timer.r or 0
end
if not durmod then
durmod = 0
end
if timer.x.gr then
self:CleanGroup(timer.x.gr, target)
end
if timer.d == 0 then
return
end
if (not self.profile.selfbars) and (target == UnitName("player") or (target == "none" and timer.k.g)) then
return
end
if self.profile.onlyself and timer.k.t ~= nil and target ~= UnitName("player") then
return
end
local id = name .. "-" .. idTarget
local slot = nil
for i = 20, 1, -1 do
if self.bars[i].id == id then
self:SetCandyBarFade(id, 0, false)
self:StopCandyBar(self.bars[i].id)
self:ReallyStopBar(self.bars[i].id)
break
end
end
for i = 1, 20 do
if not self.bars[i].id then
slot = i
break
end
end
if not slot then
slot = 20
end
self.bars[slot].id = id
self.bars[slot].timer = timer
self.bars[slot].name = name
self.bars[slot].rank = rank
self.bars[slot].target = target
self.bars[slot].group = timer.x.gr
local duration = (timer.x.d and self:GetDuration(timer.d, timer.x.d, rank, timer.cp) or timer.d) + durmod
local text = target == "none" and name or self.profile.text
text = string.gsub(text, "$t", target)
text = string.gsub(text, "$s", name)
local icon = timer.x.tx or self:GetTexture(name, timer.x)
local color = convertcolor(timer.x.cr or self.profile.barcolor)
local fade = (timer.x.rc and self.profile.ghost) and self.profile.ghost or 0.5
self:RegisterCandyBar(id, duration, text, icon, color[1], color[2], color[3], 1, 0, 0)
self:RegisterCandyBarWithGroup(id, BAR_GROUP)
if self.profile.barscale then self:SetCandyBarScale(id, self.profile.barscale) end
if self.profile.barwidth then self:SetCandyBarWidth(id, self.profile.barwidth) end
if self.profile.barheight then self:SetCandyBarHeight(id, self.profile.barheight) end
if self.profile.iconposition then self:SetCandyBarIconPosition(id, self.profile.iconposition) end
if self.profile.spacing then self:SetCandyBarGroupVerticalSpacing(BAR_GROUP, self.profile.spacing) end
if self.profile.textsize then self:SetCandyBarFontSize(id, self.profile.textsize) end
if self.profile.textcolor then local c = convertcolor(self.profile.textcolor); self:SetCandyBarTextColor(id, c[1], c[2], c[3]) end
if self.profile.bgcolor then local c = convertcolor(self.profile.bgcolor); self:SetCandyBarBackgroundColor(id, c[1], c[2], c[3], self.profile.bgalpha) end
if self.profile.bordercolor then local c = convertcolor(self.profile.bordercolor); self:SetCandyBarBorderColor(id, c[1], c[2], c[3]) end
if self.profile.bordertex then self:SetCandyBarBorderTexture(id, self.profile.bordertex) end
self:SetCandyBarFade(id, fade, true)
self:SetCandyBarCompletion(id, self.StopBar, self, id)
self:SetCandyBarReversed(id, self.profile.reverse)
self:SetCandyBarOnClick(id, function(a1, a2, a3, a4, a5) self:CandyOnClick(a1, a2, a3, a4, a5) end, timer.x.rc, timer.x.mc)
self:StartCandyBar(id, true)
end
function Chronometer:DisambiguateTarget(timer, t)
if not timer or not timer.x or not timer.x.dn then
return t
end
local base = tostring(t or "")
if base == "" then
return base
end
if string.find(base, " #", 1, true) or string.find(base, "{RT", 1, true) then
return base
end
local unit = nil
if UnitExists("pettarget") and UnitName("pettarget") == base then unit = "pettarget" end
if UnitExists("target") and UnitName("target") == base and not unit then unit = "target" end
local withIcon = nil
if unit then
local idx = GetRaidTargetIndex(unit)
if idx then
withIcon = base .. " {RT" .. idx .. "}"
end
end
local function used(label)
for i = 1, 20 do
local bar = self.bars[i]
if bar and bar.id and bar.target == label then
if not timer.x.gr or (bar.timer and bar.timer.x and bar.timer.x.gr == timer.x.gr) then
return true
end
end
end
return false
end
if withIcon and not used(withIcon) then return withIcon end
if not used(base) then return base end
for n = 2, 9 do
local candidate = base .. " #" .. n
if not used(candidate) then
return candidate
end
end
return base .. " #?"
end
function Chronometer:GetDuration(duration, record, rank, cp)
if record.rt then duration = record.rt[rank] or duration end
if record.rs then duration = duration + (rank - 1) * record.rs end
if record.cp and cp then duration = duration + (cp - 1) * record.cp end
if record.tn then
if type(record.tn) == "string" then
record.tn = self:GetTalentPosition(record.tn)
end
if record.tn then
local _, _, _, _, talentRank = GetTalentInfo(record.tn[1], record.tn[2])
if talentRank and talentRank > 0 then
local gain = record.tt and record.tt[talentRank] or (record.tb + (talentRank - 1) * (record.ts or record.tb))
duration = duration + (record.tp and (duration / 100) * gain or gain)
end
end
end
return duration
end
function Chronometer:GetTexture(name, record)
if record.xn then
name = record.xn
end
record.tx = BS:GetSpellIcon(name)
return record.tx
end
function Chronometer:GetTalentPosition(name)
for i = 1, GetNumTalentTabs() do
for j = 1, GetNumTalents(i) do
local talentName = GetTalentInfo(i, j)
if talentName == name then
return { i, j }
end
end
end
end
function Chronometer:IsBanished(target)
for i = 1, 20 do
if self.bars[i].id and self.bars[i].target == target and self.bars[i].name == BS["Banish"] then
return true
end
end
return false
end
function Chronometer:CleanGroup(group, target)
if not group or type(self.groups) ~= "table" then
return
end
local groupDef = self.groups[group]
if type(groupDef) ~= "table" then
return
end
local forall = groupDef.fa
for i = 20, 1, -1 do
if self.bars[i].group and self.bars[i].group == group then
if forall or self.bars[i].target == target then
self:StopCandyBar(self.bars[i].id)
self:StopBar(self.bars[i].id)
if not forall then break end
end
end
end
end
function Chronometer:KillBar(name, unit)
for i = 20, 1, -1 do
if self.bars[i].id and self.bars[i].name == name then
if not unit then
if self.bars[i].timer.k.t then unit = UnitName("player") else unit = "none" end
end
if self.bars[i].target == unit then
self:StopCandyBar(self.bars[i].id)
return self:StopBar(self.bars[i].id)
end
end
end
end
function Chronometer:KillBars(unit)
if unit and UnitExists("target") and UnitName("target") == unit and not UnitIsDeadOrGhost("target") then
return
end
for i = 20, 1, -1 do
if self.bars[i].id and (not unit or self.bars[i].target == unit) then
self:SetCandyBarFade(self.bars[i].id, 0.5, true)
self:StopCandyBar(self.bars[i].id)
self:ReallyStopBar(self.bars[i].id)
end
end
end
function Chronometer:StopBar(id)
if self.profile.ghost and self.profile.ghost > 0 then
self:ScheduleEvent("MTHChronometerStop" .. id, self.ReallyStopBar, self.profile.ghost, self, id)
else
self:ReallyStopBar(id)
end
end
function Chronometer:ReallyStopBar(id)
self:CancelScheduledEvent("MTHChronometerStop" .. id)
for i = 1, 20 do
if self.bars[i].id == id then
for key in pairs(self.bars[i]) do
self.bars[i][key] = nil
end
end
end
for i = 1, 19 do
if not self.bars[i].id then
local temp = self.bars[i]
for j = i + 1, 20 do
if self.bars[j].id then
self.bars[i] = self.bars[j]
self.bars[j] = temp
temp = nil
break
end
end
if temp then break end
end
end
end
function Chronometer:CandyOnClick(id, button, reactive, middlecast)
if button == "RightButton" then
MouselookStart()
Chronometer.rightclick = true
self:ScheduleEvent(function() Chronometer.rightclick = false; Chronometer:CancelScheduledEvent("MTHChronometerCheckMouselook") end, 0.5)
self:ScheduleRepeatingEvent("MTHChronometerCheckMouselook", Chronometer.onClickStopCandyBar, 0.06, self, id)
elseif button == "MiddleButton" and middlecast then
for i = 1, 20 do
if self.bars[i].id == id then
return self:CastSpellOnUnit(middlecast, self.bars[i].target)
end
end
elseif button == "LeftButton" and reactive then
for i = 1, 20 do
if self.bars[i].id == id then
return self:CastSpellOnUnit(self.bars[i].name, self.bars[i].target)
end
end
end
end
function Chronometer:onClickStopCandyBar(id)
if Chronometer.rightclick and not IsMouselooking() then
self:SetCandyBarFade(id, 0.5, true)
self:StopCandyBar(id)
self:StopBar(id)
end
end
function Chronometer:CastSpellOnUnit(spell, unit)
local restore = nil
local hadtarget = UnitExists("target")
if unit and unit ~= "none" then
if hadtarget and UnitName("target") ~= unit then
restore = true
end
TargetByName(unit, true)
end
CastSpellByName(spell)
if restore then
TargetLastTarget()
elseif not hadtarget then
ClearTarget()
end
end
function Chronometer:RunTest()
if not self._mth_enabled then
self:MTH_Enable()
end
if type(self.bars) ~= "table" then
self.bars = {}
for i = 1, 20 do
self.bars[i] = self.bars[i] or {}
end
end
for i = 1, 5 do
local name = "Test" .. i
local target = "Test"
local rank = 1
local id = name .. "-" .. target
local slot = nil
for j = 20, 1, -1 do
if self.bars[j].id == id then
self:SetCandyBarFade(id, 0, false)
self:StopCandyBar(self.bars[j].id)
self:ReallyStopBar(self.bars[j].id)
break
end
end
for j = 1, 20 do
if not self.bars[j].id then
slot = j
break
end
end
if not slot then
slot = 20
end
self.bars[slot].id = id
self.bars[slot].timer = nil
self.bars[slot].name = name
self.bars[slot].rank = rank
self.bars[slot].target = target
self.bars[slot].group = nil
local duration = math.random() * 8.0 + 2.0
local text = self.profile.text or "$t"
text = string.gsub(text, "$t", target)
text = string.gsub(text, "$s", name)
local icon = "Interface\\Icons\\Spell_Shadow_ManaBurn"
local color = convertcolor(self.profile.barcolor)
self:RegisterCandyBar(id, duration, text, icon, color[1], color[2], color[3], 1, 0, 0)
self:RegisterCandyBarWithGroup(id, BAR_GROUP)
if self.profile.barscale then self:SetCandyBarScale(id, self.profile.barscale) end
if self.profile.barwidth then self:SetCandyBarWidth(id, self.profile.barwidth) end
if self.profile.barheight then self:SetCandyBarHeight(id, self.profile.barheight) end
if self.profile.iconposition then self:SetCandyBarIconPosition(id, self.profile.iconposition) end
if self.profile.spacing then self:SetCandyBarGroupVerticalSpacing(BAR_GROUP, self.profile.spacing) end
if self.profile.textsize then self:SetCandyBarFontSize(id, self.profile.textsize) end
if self.profile.textcolor then local c = convertcolor(self.profile.textcolor); self:SetCandyBarTextColor(id, c[1], c[2], c[3]) end
if self.profile.bgcolor then local c = convertcolor(self.profile.bgcolor); self:SetCandyBarBackgroundColor(id, c[1], c[2], c[3], self.profile.bgalpha) end
if self.profile.bordercolor then local c = convertcolor(self.profile.bordercolor); self:SetCandyBarBorderColor(id, c[1], c[2], c[3]) end
if self.profile.bordertex then self:SetCandyBarBorderTexture(id, self.profile.bordertex) end
if self.profile.ghost then self:SetCandyBarFade(id, self.profile.ghost, true) end
self:SetCandyBarCompletion(id, self.StopBar, self, id)
self:SetCandyBarReversed(id, self.profile.reverse)
self:SetCandyBarOnClick(id, function(a1, a2, a3, a4, a5) self:CandyOnClick(a1, a2, a3, a4, a5) end)
self:StartCandyBar(id, true)
end
end
function Chronometer:ToggleAnchor()
if self.anchor:IsVisible() then
self.anchor:Hide()
else
self.anchor:Show()
end
end
function Chronometer:CreateAnchor(text, r, g, b)
local frame = CreateFrame("Button", "MTHChronometerAnchor", UIParent)
frame:SetWidth(220)
frame:SetHeight(25)
frame.owner = self
frame:SetClampedToScreen(true)
frame:SetMovable(true)
frame:RegisterForDrag("LeftButton")
local px = tonumber(self.profile and self.profile.barposition and self.profile.barposition.x)
local py = tonumber(self.profile and self.profile.barposition and self.profile.barposition.y)
if px ~= nil and py ~= nil then
frame:ClearAllPoints()
frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", px, py)
else
frame:SetPoint("CENTER", UIParent, "CENTER", 0, 50)
end
frame:SetScript("OnDragStart", function(self)
self = self or this
if not self or not self.StartMoving then
return
end
self:StartMoving()
end)
frame:SetScript("OnDragStop", function(self)
self = self or this
if not self then
return
end
if self.StopMovingOrSizing then
self:StopMovingOrSizing()
end
local point, _, _, x, y = self:GetPoint()
x = tonumber(x)
y = tonumber(y)
if not x or not y then
return
end
x = math.floor(x + 0.5)
y = math.floor(y + 0.5)
self:ClearAllPoints()
self:SetPoint(point or "TOPLEFT", UIParent, point or "TOPLEFT", x, y)
if self.owner and self.owner.profile and type(self.owner.profile.barposition) == "table" then
self.owner.profile.barposition.x = x
self.owner.profile.barposition.y = y
end
end)
frame:SetBackdrop({
bgFile = "Interface/Tooltips/UI-Tooltip-Background",
edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
tile = false,
tileSize = 16,
edgeSize = 16,
insets = { left = 5, right = 5, top = 5, bottom = 5 },
})
frame:SetBackdropColor(r, g, b, 0.6)
frame.Text = frame:CreateFontString(nil, "OVERLAY")
frame.Text:SetFontObject(GameFontNormalSmall)
frame.Text:ClearAllPoints()
frame.Text:SetTextColor(1, 1, 1, 1)
frame.Text:SetWidth(220)
frame.Text:SetHeight(25)
frame.Text:SetPoint("TOPLEFT", frame, "TOPLEFT")
frame.Text:SetJustifyH("CENTER")
frame.Text:SetJustifyV("MIDDLE")
frame.Text:SetText(text)
frame:Hide()
return frame
end
function Chronometer:COMBAT_DEATH(event, info)
if not self.profile.fadeonkill then return end
if info.type == "experience" then
if info.source and not self:IsBanished(info.source) then
self:ScheduleEvent(self.KillBars, 0.5, self, info.source)
return
end
elseif info.victim ~= ParserLib_SELF then
self:ScheduleEvent(self.KillBars, 0.5, self, info.victim)
return
end
end
function Chronometer:SPELL_FADE(event, info)
if not self.profile.fadeonfade then return end
if info.skill == BS["Banish"] then return end
if info.type == "fade" then
if info.victim == ParserLib_SELF then
return self:KillBar(info.skill)
end
return self:KillBar(info.skill, info.victim)
end
end
function Chronometer:SPELL_PERIODIC(event, info)
local aura, rank, unit, isgain
if info.type == "buff" then
isgain = 1
elseif info.type == "debuff" then
isgain = nil
elseif not info.isDOT then
return
end
if info.victim ~= ParserLib_SELF then
unit = info.victim
end
aura = info.skill
_, _, rank = string.find(aura, "%s([IVX]+)[^u]")
if rank then
rank = latins[rank]
aura = string.gsub(aura, "%s([IVX]+)[^u]", "")
end
if aura == "Deep Wound" then
aura = "Deep Wounds"
elseif aura == "Piercing Shots" then
aura = "Piercing Shot"
end
local timer = self.timers[self.EVENT] and self.timers[self.EVENT][aura]
if timer and timer.k.g == isgain and not info.isDOT and (timer.x.a or (timer.v and timer.v > GetTime())) then
if timer.k.t then
if not unit then unit = UnitName("player") end
if timer.k.s then
if timer.t and timer.t ~= unit then return end
else
if not UnitExists("target") or unit ~= UnitName("target") then return end
end
else
if not timer.k.s or not unit then unit = "none" else return end
end
timer.v = nil
timer.t = nil
self:StartTimer(timer, aura, unit, rank)
elseif timer and info.isDOT and not timer.x.a then
timer.v = nil
timer.t = nil
self:StartTimer(timer, aura, "none")
end
end
function Chronometer:SELF_CRITS(event, info)
if info.type == "hit" and info.source == ParserLib_SELF and info.isCrit then
for i = 1, 20 do
if self.bars[i].id and self.bars[i].timer.x.romc then
self:StartTimer(self.bars[i].timer, self.bars[i].name, self.bars[i].target, self.bars[i].rank)
end
end
end
end
function Chronometer:PLAYER_DEAD()
self.active = {}
self.captive = {}
local unit = UnitName("player")
for i = 20, 1, -1 do
if self.bars[i].id and (self.bars[i].target == unit or (self.bars[i].target == "none" and self.bars[i].timer.k.g)) then
self:SetCandyBarFade(self.bars[i].id, 0.5, true)
self:StopCandyBar(self.bars[i].id)
self:ReallyStopBar(self.bars[i].id)
end
end
end
function Chronometer:UseAction(slot, clicked, onself)
local name, rank = getActionSpell(slot)
if name then
local normalized = tostring(name)
normalized = string.gsub(normalized, "^%s+", "")
normalized = string.gsub(normalized, "%s+$", "")
local _, _, baseName = string.find(normalized, "^(.-)%s*%(.+%)$")
if baseName and baseName ~= "" then
normalized = baseName
end
local timer = nil
if self.timers[self.SPELL] then
timer = self.timers[self.SPELL][normalized] or self.timers[self.SPELL][name]
end
if timer then
self:CatchSpellcast(timer, normalized, rank, onself)
end
end
if self.hooks and type(self.hooks["UseAction"]) == "function" then
return self.hooks["UseAction"](slot, clicked, onself)
end
return callGlobal("UseAction", slot, clicked, onself)
end
function Chronometer:CastSpell(index, booktype)
local name, rankText = GetSpellName(index, booktype)
local timer = self.timers[self.SPELL] and self.timers[self.SPELL][name]
if timer then
self:CatchSpellcast(timer, name, parseRankNumber(rankText))
end
if self.hooks and type(self.hooks["CastSpell"]) == "function" then
return self.hooks["CastSpell"](index, booktype)
end
return callGlobal("CastSpell", index, booktype)
end
function Chronometer:CastSpellByName(text, onself)
local raw = tostring(text or "")
local name = raw
local rank = 0
local _, _, spellName, paren = string.find(raw, "^(.-)%s*%((.-)%)$")
if spellName and paren then
name = spellName
rank = parseRankNumber(paren)
end
name = string.gsub(name, "^%s+", "")
name = string.gsub(name, "%s+$", "")
local timer = self.timers[self.SPELL] and self.timers[self.SPELL][name]
if timer then
self:CatchSpellcast(timer, name, rank, onself)
end
if self.hooks and type(self.hooks["CastSpellByName"]) == "function" then
return self.hooks["CastSpellByName"](text, onself)
end
return callGlobal("CastSpellByName", text, onself)
end
function Chronometer:CatchSpellcast(timer, name, rank, onself)
local unit = nil
if timer.k.t then
if timer.k.s then
if onself and onself == 1 then
unit = UnitName("player")
elseif UnitExists("target") then
if timer.k.g then
if UnitIsFriend("player", "target") then unit = UnitName("target") end
else
if UnitCanAttack("player", "target") then unit = UnitName("target") end
end
end
else
if UnitExists("target") then unit = UnitName("target") else return end
end
else
unit = "none"
end
local cp = GetComboPoints("player", "target")
if cp and cp > 0 then
timer.cp = cp
end
table.insert(self.captive, { t = timer, n = name, u = unit, r = rank })
end
function Chronometer:SpellTargetUnit(unit)
for _, captive in pairs(self.captive) do
if not captive.u then
captive.u = UnitName(unit)
end
end
if self.hooks and type(self.hooks["SpellTargetUnit"]) == "function" then
return self.hooks["SpellTargetUnit"](unit)
end
return callGlobal("SpellTargetUnit", unit)
end
function Chronometer:TargetUnit(unit)
for _, captive in pairs(self.captive) do
if not captive.u then
captive.u = UnitName(unit)
end
end
if self.hooks and type(self.hooks["TargetUnit"]) == "function" then
return self.hooks["TargetUnit"](unit)
end
return callGlobal("TargetUnit", unit)
end
function Chronometer:OnMouseDown(button)
for _, captive in pairs(self.captive) do
if not captive.u and button == "LeftButton" and UnitExists("mouseover") then
captive.u = UnitName("mouseover")
end
end
if self.hooks and WorldFrame and self.hooks[WorldFrame] and type(self.hooks[WorldFrame]["OnMouseDown"]) == "function" then
return self.hooks[WorldFrame]["OnMouseDown"](WorldFrame, button)
end
if WorldFrame and type(WorldFrame.GetScript) == "function" then
local script = WorldFrame:GetScript("OnMouseDown")
if type(script) == "function" and script ~= Chronometer.OnMouseDown then
return script(WorldFrame, button)
end
end
return nil
end
function Chronometer:SPELLCAST_START()
end
function Chronometer:SPELLCAST_STOP()
local captive = self.captive[1]
if captive then
if captive.t and captive.t.x.ea then
for name, valid in pairs(captive.t.x.ea) do
local eventTimer = self.timers and self.timers[Chronometer.EVENT] and self.timers[Chronometer.EVENT][name]
if eventTimer then
eventTimer.r = captive.r
eventTimer.v = GetTime() + valid
if captive.u ~= "none" then
eventTimer.t = captive.u
end
end
end
end
if captive.u == "none" then
self:StartTimer(captive.t, captive.n, captive.u, captive.r)
else
self.active[captive.n] = { t = captive.t, n = captive.n, u = captive.u, r = captive.r }
self:ScheduleEvent(self.CompleteCast, 0.5, self, captive.n)
end
end
self.captive = {}
end
function Chronometer:CompleteCast(name)
local active = self.active[name]
if active and active.t then
self:StartTimer(active.t, active.n, active.u, active.r, -0.5)
self.active[name] = nil
end
end
function Chronometer:SpellStopCasting()
self.captive = {}
if self.hooks and type(self.hooks["SpellStopCasting"]) == "function" then
return self.hooks["SpellStopCasting"]()
end
return callGlobal("SpellStopCasting")
end
function Chronometer:SpellStopTargeting()
if type(self.captive) == "table" and table.getn(self.captive) > 0 then
local kept = {}
for i = 1, table.getn(self.captive) do
local captive = self.captive[i]
if captive and captive.t and captive.t.k and not captive.t.k.t then
table.insert(kept, captive)
end
end
self.captive = kept
else
self.captive = {}
end
if self.hooks and type(self.hooks["SpellStopTargeting"]) == "function" then
return self.hooks["SpellStopTargeting"]()
end
return callGlobal("SpellStopTargeting")
end
function Chronometer:SPELL_FAILED(event, info)
for k, captive in pairs(self.captive) do
if captive.n == info.skill then
table.remove(self.captive, k)
break
end
end
end
function Chronometer:SPELLCAST_INTERRUPTED()
for _, active in pairs(self.active) do
if active.t and active.t.x.ea then
for name in pairs(active.t.x.ea) do
local eventTimer = self.timers and self.timers[Chronometer.EVENT] and self.timers[Chronometer.EVENT][name]
if eventTimer then
eventTimer.r = nil
eventTimer.v = nil
eventTimer.t = nil
end
end
end
end
self.active = {}
end
function Chronometer:SELF_DAMAGE(event, info)
local active = self.active[info.skill]
if active and info.type == "miss" and info.victim == active.u then
if active.t and active.t.x.ea then
for name in pairs(active.t.x.ea) do
local eventTimer = self.timers and self.timers[Chronometer.EVENT] and self.timers[Chronometer.EVENT][name]
if eventTimer then
eventTimer.r = nil
eventTimer.v = nil
eventTimer.t = nil
end
end
end
self.active[info.skill] = nil
elseif active and info.type == "hit" and info.victim == active.u then
if active.t and active.t.x.ea then
for name in pairs(active.t.x.ea) do
for i = 1, 20 do
if self.bars[i].id and self.bars[i].id == name .. "-" .. info.victim then
self:StartTimer(self.bars[i].timer, self.bars[i].name, self.bars[i].target, self.bars[i].rank)
end
end
end
end
end
end
function Chronometer:SELF_HITS(event, info)
if info.type == "hit" and info.source == ParserLib_SELF then
for i = 1, 20 do
if self.bars[i].id and self.bars[i].target == info.victim and self.bars[i].timer.x.rom then
self:StartTimer(self.bars[i].timer, self.bars[i].name, self.bars[i].target, self.bars[i].rank)
end
end
end
end
function Chronometer:SPGetGUIDForName(base)
local name = tostring(base or "")
if name == "" then return nil end
local function guidOf(unit)
if not UnitExists(unit) then
return nil
end
if UnitName(unit) ~= name then
return nil
end
if type(UnitGUID) == "function" then
local guid = UnitGUID(unit)
if guid and guid ~= "" then
return guid
end
end
return nil
end
return guidOf("pettarget") or guidOf("target") or guidOf("mouseover")
end