Nenue@6: --- Combat Nenue@6: -- @file-author@ Nenue@6: -- @project-revision@ @project-hash@ Nenue@6: -- @file-revision@ @file-hash@ Nenue@6: -- Created: 1/11/2016 4:27 PM Nenue@6: --- Data collector for API Combat Log Event Nenue@6: local T, _G = Turok, _G Nenue@6: local mod = T:NewModule("Combat") Nenue@6: local pairs, ipairs, select, concat, format, tinsert, wipe = pairs, ipairs, select, table.concat, string.format, tinsert, table.wipe Nenue@6: local GetSpellTexture, CreateFrame, GetTime, unpack, floor = GetSpellTexture, CreateFrame, GetTime, unpack, floor Nenue@6: local db Nenue@6: local cText, cNum, cKey, cWord, cPink, cType, cBool = cText, cNum, cKey, cWord, cPink, cType, cBool Nenue@6: local print = function(...) Nenue@6: if _G.Devian and _G.DevianDB.workspace ~= 1 then Nenue@6: _G.print('CombatText', ...) Nenue@6: end Nenue@6: end Nenue@6: Nenue@6: Nenue@6: --- performance constants Nenue@6: local SCT_NAME = 'TurokSCT%s' Nenue@6: local SCT_TEMPLATE = 'TurokSCTAnchorTemplate' Nenue@6: local SCT_PREGAME = 5 Nenue@6: local SCT_MESSAGE_TEMPLATE = 'TurokCombatMessageTemplate' Nenue@6: local SCT_MESSAGE_NAME = 'CombatString%d' Nenue@6: local LOG_TEMPLATE = 'TurokCombatLogAnchorTemplate' Nenue@6: Nenue@6: --- UX structures Nenue@6: T.defaults.CombatText = { Nenue@6: width = 400, height = 800, Nenue@6: parent = 'UIParent', Nenue@6: Outgoing = { Nenue@6: anchor = 'RIGHT', anchorTo = 'RIGHT', Nenue@6: x = -400, y = -200, Nenue@6: }, Nenue@6: Incoming = { Nenue@6: anchor = 'LEFT', anchorTo = 'LEFT',parent = 'UIParent', Nenue@6: x = 400, y = -200, Nenue@6: }, Nenue@6: DoTTracker = { Nenue@6: anchor = 'BOTTOMLEFT', anchorTo = 'BOTTOM', parent = 'UIParent', Nenue@6: x = 80, y = 400, Nenue@6: }, Nenue@6: defaultFont = {"Interface\\Addons\\Turok\\Media\\font\\ArchivoNarrow-Regular.ttf", 18, 'OUTLINE'}, Nenue@6: defaultAnimation = 'slide', Nenue@6: Nenue@6: textFonts = { Nenue@6: font1 = {"Interface\\Addons\\Turok\\Media\\font\\ArchivoNarrow-Regular.ttf" , 30, 'OUTLINE'}, Nenue@6: font2 = {"Interface\\Addons\\Turok\\Media\\font\\ArchivoNarrow-Regular.ttf" , 24, 'OUTLINE'}, Nenue@6: font3 = {"Interface\\Addons\\Turok\\Media\\font\\ArchivoNarrow-Bold.ttf" , 20, 'OUTLINE'}, Nenue@6: }, Nenue@6: Nenue@6: --- [(string) combatEvent] = {[1] = (string) format [, [2] = fontKey]} Nenue@6: --- Substitution values Nenue@6: -- %d [1] amount [2] overkill [3] absorbed [4] blocked Nenue@6: -- %s [5] spell [6] caster [7] school Nenue@6: textFormat = { Nenue@6: ['SWING_DAMAGE'] = {'%d'}, Nenue@6: ['SPELL_DAMAGE'] = {'%d'}, Nenue@6: ['RANGE_DAMAGE'] = {'%d'}, Nenue@6: Incoming = { Nenue@6: ['SWING_DAMAGE'] = {'-%d'}, Nenue@6: ['SPELL_DAMAGE'] = {'-%d (%s)'}, Nenue@6: ['RANGE_DAMAGE'] = {'-%d'}, Nenue@6: }, Nenue@6: }, Nenue@6: textModifiers ={ Nenue@6: critical = {'%s!', 'pop'}, Nenue@6: overKill = {'%s |cFF0088FFKilling Blow!|r', 'slide'}, Nenue@6: multistrike = {'<%s>', 'lateralSlide', 'font3'}, Nenue@6: absorbed = {'%s (%d)', 'slide'}, Nenue@6: blocked = {'%s {%d}', 'slide'}, Nenue@6: pet = {'(%s)', 'lateralSlide'}, Nenue@6: grouped = {'%s (%d hit)', 'slide'}, Nenue@6: }, Nenue@6: --- [AnimationGroup key] = {[XML attrib] = [value], d[x/y] = (number) 0-1 } Nenue@6: -- d[x/y] indicates the proportional relevance of each FontString dimension when frames are displaced by a new event Nenue@6: -- x/y indicates the ranges of movement made by an animation, and are also considered when calculating displacement Nenue@6: animation = { Nenue@6: slide = { Nenue@6: x = 0, dx = 0, Nenue@6: y = 300, dy = 1, Nenue@6: duration = 2 Nenue@6: }, Nenue@6: lateralSlide = { Nenue@6: x = 300, dx = 1, Nenue@6: y = 0, dy = 0, Nenue@6: duration = 2 Nenue@6: }, Nenue@6: pop = { Nenue@6: toScale = 1.4, Nenue@6: fromScale = 0.1, Nenue@6: duration = 0.14, Nenue@6: dx = 0, dy = 1, Nenue@6: }, Nenue@6: fadeOut = {change = -1, duration = 0.5}, Nenue@6: }, Nenue@6: } Nenue@6: --- [1] text wrapper [2] animation type Nenue@6: local dotEvents = { Nenue@6: SPELL_AURA_APPLIED = true, Nenue@6: SPELL_PERIODIC_DAMAGE = true, Nenue@6: SPELL_AURA_REMOVED = true, Nenue@6: SPELL_AURA_REFRESHED = true, Nenue@6: } Nenue@6: local petGUID = {} Nenue@6: Nenue@6: ------------- data structures Nenue@6: local defaultAnimation, defaultFont Nenue@6: local criticalModifier, absorbedModifier, blockedModifier, overKillModifier, multistrikeModifier, groupedModifier Nenue@6: local criticalAnimation, absorbedAnimation, blockedAnimation, overKillAnimation, multistrikeAnimation, groupedAnimation Nenue@6: local criticalFont, absorbedFont, blockedFont, overKillFont,multistrikeFont, groupedFont Nenue@6: local textFonts = {} Nenue@6: local textFormat = {} Nenue@6: local animation = {} Nenue@6: local spellCache = {} Nenue@6: local DoTFrames = {} Nenue@6: Nenue@6: local dotTrackingEvents = { Nenue@6: ['SPELL_AURA_APPLIED'] = true, Nenue@6: ['SPELL_AURA_REMOVED'] = true, Nenue@6: ['SPELL_AURA_REFRESHED'] = true, Nenue@6: } Nenue@6: Nenue@6: --- multi-hit events data Nenue@6: -- [(string) localized name] = { Nenue@6: -- [1] = (bool) controlled channel Nenue@6: -- [2] = (number) minimum time to wait for next damage tick Nenue@6: -- [3] = (number) maximum time to log grouped spell hits Nenue@6: -- [4] = (string) combatEvent full string Nenue@6: -- [5] = (string) combatEvent short prefix (as opposed to SPELL_PERIODIC) Nenue@6: -- [6] = (string) combatEvent short suffix (as opposed to AURA_APPLIED) Nenue@6: -- } Nenue@6: -- tailing fields are set to reduce the number of arguments passed around Nenue@6: local groupedSpells = { Nenue@6: global = {false, 1, 'SPELL_PERIODIC_DAMAGE', 'SPELL', 'DAMAGE'}, Nenue@6: ['Crimson Tempest'] = {false, 0.3, 'SPELL_DAMAGE', 'SPELL', 'DAMAGE'}, -- shockwave dot Nenue@6: ['Mind Sear'] = {false, 1, 'SPELL_DAMAGE', 'SPELL', 'DAMAGE'}, -- scanning aoe Nenue@6: ['Searing Insanity'] = {false, 1, 'SPELL_DAMAGE', 'SPELL', 'DAMAGE'}, -- scanning aoe Nenue@6: ['Ice Nova'] = {false, 0.4, 'SPELL_DAMAGE', 'SPELL', 'DAMAGE'}, -- shockwave aoe Nenue@6: ['Blizzard'] = {false, 1, 'SPELL_DAMAGE', 'SPELL', 'DAMAGE'}, -- scanning aoe Nenue@6: ['Frozen Orb'] = {false, 1, 'SPELL_DAMAGE', 'SPELL', 'DAMAGE'}, Nenue@6: ['Ice Bomb'] = {false, 0.4, 'SPELL_DAMAGE', 'SPELL', 'DAMAGE'}, -- shockwave aoe Nenue@6: ['Comet Storm'] = {false, 2.5, 'SPELL_DAMAGE', 'SPELL', 'DAMAGE'}, -- staggered multi-hit Nenue@6: ['Barrage'] = {false, 1.1, 'SPELL_DAMAGE', 'SPELL', 'DAMAGE'}, -- projectile vommit Nenue@6: ['Glaive Toss'] = {false, 3, 'SPELL_DAMAGE', 'SPELL', 'DAMAGE'}, -- path projectile, Nenue@6: ['Doom Nova'] = {false, 0.4, 'SPELL_DAMAGE', 'SPELL', 'DAMAGE'} Nenue@6: } Nenue@6: Nenue@6: local dotSpellIndex = { Nenue@6: ['Shadow Word: Pain'] = {'HARMFUL|PLAYER'}, Nenue@6: ['Vampiric Touch'] = {'HARMFUL|PLAYER'} Nenue@6: } Nenue@6: Nenue@6: local offhandSpellIndex = { Nenue@6: ['Execute'] = {'Execute Off-Hand'}, Nenue@6: ['Mutilate'] = {'Mutilate Off-Hand'}, Nenue@6: ['Stormstrike'] = {'Stormstrike Off-Hand'}, Nenue@6: } Nenue@6: Nenue@6: --- stored as a list of field representations to be concatenated together within a loadstring def Nenue@6: Nenue@6: Nenue@6: Nenue@6: --- Tracking tables Nenue@6: local groupedQueue = {} Nenue@6: local groupedEvents = {} Nenue@6: local offhandQueue = {} Nenue@6: local sct_format = {} Nenue@6: Nenue@6: Nenue@6: local function GetGUIDInfo(guid) Nenue@6: local unitType, flags1, flags2 = guid:match('(%a+)\-(%x+)\-(%x+)') Nenue@6: return concat({unitType, flags1, flags2},'|r::|cFF0088FF')..'|r' Nenue@6: end Nenue@6: Nenue@6: local SpellSchoolColors = { Nenue@6: [1] = {255, 255,0}, -- physical Nenue@6: [2] = {255, 230, 128}, -- holy Nenue@6: [3] = {255, 255, 128}, -- holy+phys Nenue@6: [4] = {255, 128, 0}, -- fire Nenue@6: [8] = {77, 255, 77}, -- nature Nenue@6: [16] = {128, 255, 255}, -- frost Nenue@6: [20] = {255, 192, 128}, -- frostfire Nenue@6: [32] = {128, 128, 255}, -- shadow Nenue@6: [48] = {128, 192, 255}, -- shadow+frost Nenue@6: [64] = {255, 128, 255}, -- arcane, Nenue@6: [72] = {255, 128, 128}, -- spellstorm, Nenue@6: } Nenue@6: Nenue@6: local h = '|cFF%02X%02X%02X' Nenue@6: local SpellSchoolColor = function (flags, spellName) Nenue@6: local i = 64 Nenue@6: local rA, gA, bA Nenue@6: if SpellSchoolColors[flags] then Nenue@6: print(flags, 'match') Nenue@6: print(format('%02X%02X%02X', unpack(SpellSchoolColors[flags]))) Nenue@6: return format(h, unpack(SpellSchoolColors[flags])) Nenue@6: end Nenue@6: Nenue@6: repeat Nenue@6: local rB, gB, bB = unpack(SpellSchoolColors[i]) Nenue@6: if i <= flags then Nenue@6: if not rA then Nenue@6: rA = rB Nenue@6: gA = gB Nenue@6: bA = bB Nenue@6: else Nenue@6: rA = (rA+rB)/2 Nenue@6: gA = (gA+gB)/2 Nenue@6: bA = (bA+bB)/2 Nenue@6: end Nenue@6: print('test:', cWord(i), '<=', cKey(flags), '=', (i <= flags), cPink(rA), cNum(gA), cText(bA)) Nenue@6: flags = flags - i Nenue@6: else Nenue@6: print(i, 'skip') Nenue@6: end Nenue@6: i = i/2 Nenue@6: until (i == 1) Nenue@6: SpellSchoolColors[flags] = {rA, gA, bA } Nenue@6: --print(string.format('%02X%02X%02X', unpack(SpellSchoolColors[flags]))) Nenue@6: return format(h, unpack(SpellSchoolColors[flags])) Nenue@6: end Nenue@6: local myGUID Nenue@6: Nenue@6: mod.NewCombatMessage = function(frame, index) Nenue@6: local ct = CreateFrame('Frame', SCT_MESSAGE_NAME:format(frame.lineID), frame, SCT_MESSAGE_TEMPLATE) Nenue@6: index = index and 'active' or 'expired' Nenue@6: frame.lineID = frame.lineID + 1 Nenue@6: frame.active[#frame.active+1] = ct Nenue@6: ct.index = #frame.active Nenue@6: ct.x = 0 Nenue@6: ct.y = 0 Nenue@6: ct.point = 'BOTTOMLEFT' Nenue@6: return ct Nenue@6: end Nenue@6: Nenue@6: --- frame interaction logic Nenue@6: mod.AddCombatMessage = function(frame, text, icon, animationType, fontKey) Nenue@6: local line Nenue@6: print(fontKey) Nenue@6: Nenue@6: local expired = frame.expired Nenue@6: local active = frame.active Nenue@6: local a = 1 Nenue@6: local shiftY, shiftX Nenue@6: local lastFrame Nenue@6: --- If animation has overlap delta values, find the last active frame of that animation type and check for overlaps. Nenue@6: --- Frames are considered overlapping when the last member's ((height - distance traveled) * delta) is over 0. Nenue@6: --- This assumes the same string height for the upcoming frame since wordwrap isn't enabled. Nenue@6: print(animationType) Nenue@6: if animation[animationType].dx or animation[animationType].dy then Nenue@6: print('animation has displacement') Nenue@6: if frame.last then Nenue@6: lastFrame = frame.last Nenue@6: local dp = lastFrame[animationType]:GetProgress() Nenue@6: if animation[animationType].dx then Nenue@6: local dx = dp * animation[animationType].x Nenue@6: shiftX = (lastFrame.string:GetStringWidth() - dx) * animation[animationType].dx Nenue@6: end Nenue@6: if animation[animationType].dy then Nenue@6: local dy = dp * animation[animationType].y Nenue@6: shiftY = (lastFrame.string:GetStringHeight() - dy) * animation[animationType].dy Nenue@6: print(' ', 'h=', floor(lastFrame.string:GetStringHeight()), 'dY=', dy, 'offsetY=', shiftY) Nenue@6: end Nenue@6: print(cWord('lastFrame hit:'), lastFrame and lastFrame:GetName(), cNum(shiftX), cNum(shiftY)) Nenue@6: end Nenue@6: end Nenue@6: Nenue@6: -- find a usable frame Nenue@6: local currentFrame Nenue@6: for i, ct in ipairs(frame.active) do Nenue@6: if not currentFrame then Nenue@6: if ct.discard then Nenue@6: ct.discard= nil Nenue@6: currentFrame = ct Nenue@6: end Nenue@6: end Nenue@6: Nenue@6: if lastFrame and ct.animationType == animationType and not ct.discard then Nenue@6: --print('lastFrame defined, check for overlap') Nenue@6: if shiftY > 0 then Nenue@6: print(cWord(' * vertical shift'), cNum('+'..floor(shiftY))) Nenue@6: ct.y = ct.y + shiftY Nenue@6: end Nenue@6: if shiftX > 0 then Nenue@6: print(cWord(' * horizontal shift'), cNum('+'..floor(shiftX))) Nenue@6: ct.x = ct.x + shiftX Nenue@6: end Nenue@6: ct:SetPoint(ct.point, frame, ct.point, ct.x, ct.y) Nenue@6: end Nenue@6: end Nenue@6: -- if no expired frames became available, make a new one (should max at 20 or so if groupings are right) Nenue@6: if not currentFrame then Nenue@6: currentFrame = mod.NewCombatMessage(frame) Nenue@6: print(cNum(' creating new string object for the heap')) Nenue@6: end Nenue@6: Nenue@6: print(cText(' * Starting'), cPink(animationType), 'on', cKey(currentFrame:GetName()), ' ['..cNum(currentFrame.index)..']') Nenue@6: Nenue@6: if icon then Nenue@6: currentFrame.icon:Show() Nenue@6: currentFrame.icon:SetTexture(icon) Nenue@6: else Nenue@6: currentFrame.icon:Hide() Nenue@6: end Nenue@6: if fontKey then Nenue@6: local newFont = fontKey and textFonts[fontKey] or defaultFont Nenue@6: Nenue@6: local path, size, flags = currentFrame.string:GetFont() Nenue@6: path = newFont[1] or path Nenue@6: size = newFont[2] or size Nenue@6: flags = newFont[3] or flags Nenue@6: print(cText('font ('..cWord(fontKey)..'):'), path, size, flags) Nenue@6: local result = currentFrame.string:SetFont(path, size, flags) Nenue@6: print(cNum(' result:'), cNum(result), currentFrame.string:GetFont()) Nenue@6: --currentFrame.fontKey = fontKey Nenue@6: end Nenue@6: Nenue@6: currentFrame.animationType = animationType Nenue@6: currentFrame.string:SetText(text) Nenue@6: local cHeight = currentFrame.string:GetStringHeight() Nenue@6: currentFrame:SetSize(currentFrame.string:GetStringWidth(), cHeight) Nenue@6: currentFrame.icon:SetSize(cHeight, cHeight) Nenue@6: currentFrame.y = 0 Nenue@6: currentFrame.x = 0 Nenue@6: currentFrame:SetPoint(currentFrame.point, frame, currentFrame.point, currentFrame.x, currentFrame.y) Nenue@6: currentFrame[animationType]:Play() Nenue@6: frame.last = currentFrame Nenue@6: end Nenue@6: Nenue@6: local GetTextFormatFunc = function(amount, overKill, absorbed, blocked, multistrike, spellName, sourceName, destName) Nenue@6: local sub_text = { Nenue@6: ['%d'] = amount, Nenue@6: ['%o'] = overKill, Nenue@6: ['%a'] = absorbed, Nenue@6: ['%b'] = blocked, Nenue@6: ['%m'] = multistrike, Nenue@6: ['%s'] = spellName, Nenue@6: ['%n'] = sourceName, Nenue@6: ['%t'] = destName, Nenue@6: } Nenue@6: local func = function(token) Nenue@6: print(cPink('gsub run:'), token) Nenue@6: return sub_text[token] Nenue@6: end Nenue@6: return func Nenue@6: end Nenue@6: Nenue@6: --- builds CT messages from combat log event data Nenue@6: -- separated from frame interaction for auxiliary events such as combat/loot/etc Nenue@6: local CreateDamageText = function(frame, timestamp, combatEvent, sourceName, destName, spellID, spellName, effectSchool, amount, overKill, resisted, blocked, absorbed, critical, glancing, crushing, isOffHand, multistrike, ticks) Nenue@6: if combatEvent:match('^SWING') then Nenue@6: spellID = 1 Nenue@6: spellName = 'Attack' Nenue@6: elseif combatEvent:match('^RANGE') then Nenue@6: spellID = 2 Nenue@6: spellName = 'Auto Shot' Nenue@6: end Nenue@6: if not combatEvent:match('DAMAGE') then Nenue@6: return Nenue@6: end Nenue@6: Nenue@6: Nenue@6: local fontKey = 'font1' Nenue@6: local icon = GetSpellTexture(spellID) Nenue@6: local animationType = defaultAnimation Nenue@6: local text = amount Nenue@6: if textFormat[combatEvent] then Nenue@6: local tString, fontKey = unpack(textFormat[combatEvent]) Nenue@6: if amount > 1000000 then Nenue@6: amount = (floor(amount/100000)/10) ..'M' Nenue@6: elseif amount > 1000 then Nenue@6: amount = (floor(amount/100)/10) ..'k' Nenue@6: end Nenue@6: Nenue@6: text = tString:gsub('%%[doabsnt]', GetTextFormatFunc(amount, overKill, absorbed, blocked, multistrike, spellName, sourceName, destName)) Nenue@6: print("** font override:", '"'..cText(tString)..'",', cPink(font), cNum(size), cWord(outline)) Nenue@6: end Nenue@6: Nenue@6: print('** getting color data', cText(spellName)) Nenue@6: if type(effectSchool) == 'number' then Nenue@6: text = SpellSchoolColor(effectSchool) ..text..'|r' Nenue@6: end Nenue@6: Nenue@6: if overKill > 0 then Nenue@6: text = overKillModifier:format(text, overKill) Nenue@6: animationType = overKillAnimation Nenue@6: fontKey = overKillFont or fontKey Nenue@6: end Nenue@6: if critical then Nenue@6: text = criticalModifier:format(text) Nenue@6: animationType = criticalAnimation Nenue@6: fontKey = criticalFont or fontKey Nenue@6: end Nenue@6: if absorbed then Nenue@6: text = absorbedModifier:format(text, absorbed) Nenue@6: animationType = absorbedAnimation Nenue@6: fontKey = absorbedFont or fontKey Nenue@6: end Nenue@6: if blocked then Nenue@6: text = blockedModifier:format(text, blocked) Nenue@6: animationType = blockedAnimation Nenue@6: fontKey = blockedFont or fontKey Nenue@6: end Nenue@6: if multistrike then Nenue@6: text = multistrikeModifier:format(text, multistrike) Nenue@6: animationType = multistrikeAnimation Nenue@6: fontKey = multistrikeFont or fontKey Nenue@6: end Nenue@6: Nenue@6: print(ticks) Nenue@6: if ticks then Nenue@6: text = groupedModifier:format(text, ticks) Nenue@6: animationType = groupedAnimation Nenue@6: fontKey = groupedFont or fontKey Nenue@6: end Nenue@6: Nenue@6: Nenue@6: print('** sending format to SCT:', text, icon, animationType, fontKey) Nenue@6: Nenue@6: Nenue@6: mod.AddCombatMessage(frame, text .. '|r', icon, animationType, fontKey) Nenue@6: end Nenue@6: Nenue@6: local CT_ShowConsolidated = function() Nenue@6: for spellName, queuedSpell in pairs(groupedQueue) do Nenue@6: local isChannel, minDelay, combatEvent, sourceName, destName, spellID, school, SCT, timestamp = unpack(queuedSpell) Nenue@6: for i, v in ipairs(queuedSpell) do Nenue@6: print(' ', i, '=', v) Nenue@6: end Nenue@6: Nenue@6: print(spellName, 'vars:', spellID, spellName, school, SCT, timestamp) Nenue@6: if groupedEvents[spellName] and #groupedEvents[spellName] ~= 0 then Nenue@6: local amount, overKill, resisted, blocked, absorbed, critical, glancing, crushing, multistrike, hit = 0, -1, 0, 0, 0, 0, 0, 0, 0, 0 Nenue@6: for i, line in ipairs(groupedEvents[spellName]) do Nenue@6: -- extra strike? Nenue@6: --{amount, overKill, resisted, blocked, absorbed, critical, glancing, crushing, multistrike} Nenue@6: local amount_n, overKill_n, resisted_n, blocked_n, absorbed_n, critical_n, glancing_n, crushing_n, multi_n, sourceGUID, destGUID, sourceName, destName = unpack (line) Nenue@6: Nenue@6: amount = amount + amount_n Nenue@6: if overKill_n > 0 then overKill = overKill + overKill_n end Nenue@6: if blocked_n then blocked = blocked + line[4] end Nenue@6: if absorbed_n then absorbed = absorbed + line[5] end Nenue@6: if critical_n then critical = critical + 1 end Nenue@6: if multi_n then multistrike = multistrike + 1 end Nenue@6: Nenue@6: hit = hit + 1 Nenue@6: end Nenue@6: if overKill == -1 then Nenue@6: overKill = overKill + 1 Nenue@6: end Nenue@6: wipe(groupedEvents[spellName]) Nenue@6: groupedQueue[spellName] = nil Nenue@6: print(' expelling', spellName, cText('A:'), cNum(amount), cText('O:'), cNum(overKill), cText('Ticks:'), cNum(hit), cText('Crits:'), cNum(critical), cText(school)) Nenue@6: print(SCT, timestamp, combatEvent, sourceName, destName, spellID, spellName, school, amount, overKill, resisted, blocked, absorbed, critical, glancing, crushing, multistrike, hit) Nenue@6: CreateDamageText(SCT, timestamp, combatEvent, sourceName, destName, spellID, spellName, school, amount, -1, nil, nil, nil, false, glancing, crushing, false, multistrike, hit) Nenue@6: end Nenue@6: end Nenue@6: end Nenue@6: Nenue@6: local CT_ConsolidateText = function (self, timestamp, sourceGUID, destGUID, sourceName, destName, spellID, spellName, school, amount, overKill, resisted, blocked, absorbed, critical, glancing, crushing, isOffHand, multistrike) Nenue@6: local archive = groupedSpells[spellName] and groupedSpells[spellName] or groupedSpells.global Nenue@6: local isChannel, minDelay, combatEvent = unpack(archive) Nenue@6: if not groupedEvents[spellName] then Nenue@6: groupedEvents[spellName] = {} Nenue@6: end Nenue@6: Nenue@6: if #groupedEvents[spellName] == 0 then Nenue@6: groupedQueue[spellName] = {isChannel, minDelay, combatEvent, sourceName, destName, spellID, school, self, timestamp} Nenue@6: T:ScheduleTimer(CT_ShowConsolidated, minDelay) Nenue@6: print(' starting archive for', select(7, unpack(archive))) Nenue@6: else Nenue@6: print(' recording into archive', cText(spellName)) Nenue@6: end Nenue@6: tinsert(groupedEvents[spellName], {amount, overKill, resisted, blocked, absorbed, critical, glancing, crushing, multistrike, sourceGUID, destGUID, sourceName, destName, sourceName, destName,}) Nenue@6: end Nenue@6: Nenue@6: local CT_OnCast = function(self, event, unit, spellName, lineID, spellID) Nenue@6: if unit ~= 'player' and unit ~= 'pet' then Nenue@6: return Nenue@6: end Nenue@6: Nenue@6: if groupedSpells[spellName] then Nenue@6: print(cText('** Spell casting info received')) Nenue@6: local isChannel, delay = unpack(groupedSpells[spellName]) Nenue@6: if isChannel and event == 'CHANNEL_STOP' and groupedEvents[spellName] then Nenue@6: T:ScheduleTimer(CT_ShowConsolidated, delay) Nenue@6: else Nenue@6: if not groupedEvents[spellName] then Nenue@6: groupedEvents[spellName] = {} Nenue@6: end Nenue@6: end Nenue@6: end Nenue@6: end Nenue@6: Nenue@6: local CT_Unlock = function(str) Nenue@6: for i, CT in pairs(mod.CombatTextFrames) do Nenue@6: local frame = CT.frame Nenue@6: frame.configMode = (frame.configMode == nil) and true or nil Nenue@6: frame:RegisterForDrag(frame.configMode and 'LeftButton' or nil) Nenue@6: frame:EnableMouse(frame.configMode and true or false) Nenue@6: print(i, frame.configMode and 'ON' or 'OFF') Nenue@6: for _, reg in ipairs(frame.configRegions) do Nenue@6: if frame.configMode then Nenue@6: reg:Show() Nenue@6: else Nenue@6: reg:Hide() Nenue@6: end Nenue@6: end Nenue@6: Nenue@6: if frame.configMode then Nenue@6: CT.configTimer = T:ScheduleRepeatingTimer(function() Nenue@6: print(i, 'config tick') Nenue@6: for i, s in ipairs(CT.sample) do Nenue@6: CT.OnEvent(frame, unpack(s)) Nenue@6: end Nenue@6: end, 2) Nenue@6: else Nenue@6: print(CT.configTimer) Nenue@6: T:CancelTimer(CT.configTimer) Nenue@6: end Nenue@6: Nenue@6: end Nenue@6: end Nenue@6: Nenue@6: --- check for sectors Nenue@6: local queue = {} Nenue@6: mod.OnDamage = function(self, event, ...) Nenue@6: local isVisible Nenue@6: Nenue@6: local timestamp, combatEvent = ... Nenue@6: print(cText('* CT'), cKey(combatEvent)) Nenue@6: if combatEvent == 'UNIT_DIED' then Nenue@6: return Nenue@6: end Nenue@6: local sourceGUID, sourceName, sourceFlags, _, destGUID, destName, destFlags, _, spellID, spellName, spellSchool = select(4, ...) Nenue@6: print(' from', cPink(sourceGUID), 'spell', cNum(spellID), cText(spellName), '->', cKey(destGUID)) Nenue@6: Nenue@6: -- SWING starts at arg 10, SPELL/RANGE start at arg 13, ENVIRONMENTAL starts at arg 8 Nenue@6: local offset = 15 Nenue@6: print(combatEvent:sub(0,3)) Nenue@6: if combatEvent:sub(0,3) == 'SWI' then Nenue@6: offset = 10 Nenue@6: elseif combatEvent:sub(0,3) == 'ENV' then Nenue@6: offset = 8 Nenue@6: end Nenue@6: local amount, overKill, effectSchool, resisted, blocked, absorbed, critical, glancing, crushing, isOffHand, multistrike = select(offset, ...) Nenue@6: Nenue@6: print(' dmg', amount, overKill) Nenue@6: Nenue@6: local sc = SpellSchoolColors[effectSchool] Nenue@6: if groupedSpells[spellName] then Nenue@6: print('* ', cText(spellName), 'to consolidator') Nenue@6: CT_ConsolidateText(self, timestamp, sourceGUID, destGUID, sourceName, destName, spellID, spellName, effectSchool, amount, overKill, resisted, blocked, absorbed, critical, glancing, crushing, isOffHand, multistrike) Nenue@6: return Nenue@6: end Nenue@6: Nenue@6: print('displaying on', cWord(self:GetName())) Nenue@6: CreateDamageText(self, timestamp, combatEvent, sourceName, destName, spellID, spellName, effectSchool, amount, overKill, resisted, blocked, absorbed, critical, glancing, crushing, isOffHand, multistrike) Nenue@6: end Nenue@6: Nenue@6: mod.maxDotFrame = 1 Nenue@6: mod.OnDotEvent = function (self, event, timestamp, combatEvent, ...) Nenue@6: print(cWord('DOT'), combatEvent) Nenue@6: local sourceGUID, sourceName, sourceFlags, _, destGUID, destName, destFlags, destExtraFlags, spellID, spellName, castSchool, amount, overKill, resisted, blocked, absorbed, critical, glancing, crushing, isOffHand, multistrike = select(2, ...) Nenue@6: print(cText('dot from'), cWord(sourceGUID), 'to', cKey(destGUID)) Nenue@6: if sourceGUID == T.GUID then Nenue@6: local p = mod.PeriodicTable Nenue@6: p[destGUID] = p[destGUID] or {} Nenue@6: local unit = p[destGUID] Nenue@6: if not p[spellID] then Nenue@6: if not p.frames[#p.frames] then Nenue@6: unit[spellID] = CreateFrame('Frame', 'DotBar'..mod.maxDotFrame, p, 'TkDotBarTemplate') Nenue@6: mod.maxDotFrame = mod.maxDotFrame + 1 Nenue@6: unit[spellID].dotID = mod.maxDotFrame Nenue@6: Nenue@6: print(' create new frame ['..cKey(unit[spellID]:GetID(), '] for'), cWord(spellName), cKey(destGUID)) Nenue@6: else Nenue@6: unit[spellID] = p.frames[#p.frames] Nenue@6: p.frames[#p.frames] = nil -- remove that entry Nenue@6: print(' recycling frame ['..cKey(unit[spellID].dotID, '] for'), cWord(spellName), cKey(destGUID)) Nenue@6: end Nenue@6: Nenue@6: unit[spellID]:Show() Nenue@6: end Nenue@6: local dot = unit[spellID] Nenue@6: local time = GetTime() Nenue@6: end Nenue@6: end Nenue@6: Nenue@6: Nenue@6: Nenue@6: Nenue@6: function mod:OnInitialize() Nenue@6: print('This is a thing.') Nenue@6: self.UNIT_SPELLCAST_SUCCEEDED = CT_OnCast Nenue@6: self.UNIT_SPELLCAST_CHANNEL_START = CT_OnCast Nenue@6: self.UNIT_SPELLCAST_CHANNEL_STOP = CT_OnCast Nenue@6: end Nenue@6: Nenue@6: function mod:OnInitialize() Nenue@6: mod.db = TurokData.CombatText Nenue@6: db = TurokData.CombatText Nenue@6: myGUID = T.GUID Nenue@6: mod.PeriodicTable = mod.PeriodicTable or CreateFrame('Frame', 'TurokPeriodicFrame', UIParent) Nenue@6: mod.PeriodicTable.frames = {} Nenue@6: Nenue@6: local m = mod.db.textModifiers Nenue@6: --- These values are going to be looked up a lot, so cache as close as possible Nenue@6: criticalModifier = m.critical[1] or '%s CRIT' Nenue@6: criticalAnimation = m.critical[2] or 'slide' Nenue@6: criticalFont = m.critical[3] Nenue@6: Nenue@6: absorbedModifier = m.absorbed[1] or '%s ABS' Nenue@6: absorbedAnimation = m.absorbed[2] or 'slide' Nenue@6: absorbedFont = m.absorbed[3] Nenue@6: Nenue@6: blockedModifier = m.blocked[1] or '%s BLK' Nenue@6: blockedAnimation = m.blocked[2] or 'slide' Nenue@6: blockedFont = m.blocked[3] Nenue@6: Nenue@6: overKillModifier = m.overKill[1] or '%s KILL' Nenue@6: overKillAnimation = m.overKill[2] or 'slide' Nenue@6: overKillFont = m.overKill[3] Nenue@6: Nenue@6: multistrikeModifier = m.multistrike[1] or '%s MS' Nenue@6: multistrikeAnimation = m.multistrike[2] or 'slide' Nenue@6: multistrikeFont = m.multistrike[3] Nenue@6: Nenue@6: groupedModifier = m.grouped[1] or '%s' Nenue@6: groupedAnimation = m.grouped[2] or 'slide' Nenue@6: groupedFont = m.grouped[3] Nenue@6: Nenue@6: --- Same as above, but for specific table values, key is determined by the combat event Nenue@6: defaultAnimation = mod.db.defaultAnimation Nenue@6: defaultFont = mod.db.defaultFont Nenue@6: for k,v in pairs(mod.db.textFormat) do Nenue@6: textFormat[k] = {v[1], v[2]} Nenue@6: print('imported textFormat.'..k, cText(textFormat[k][1]), cNum(textFormat[k][2])) Nenue@6: end Nenue@6: for k,v in pairs(mod.db.textFonts) do Nenue@6: textFonts[k] = {v[1] or defaultFont[1], v[2] or defaultFont[2], v[3] or defaultFont[3]} Nenue@6: print('imported font.'..k, cText(textFonts[k][1]), cNum(textFonts[k][2]), cWord(textFonts[k][3])) Nenue@6: end Nenue@6: Nenue@6: for k,v in pairs(mod.db.animation) do Nenue@6: animation[k] = {} Nenue@6: animation[k].x = v.x Nenue@6: animation[k].y = v.y Nenue@6: animation[k].dx = v.dx Nenue@6: animation[k].dy = v.dy Nenue@6: animation[k].fromScale = v.fromScale Nenue@6: animation[k].toScale = v.toScale Nenue@6: animation[k].deviation = v.deviation Nenue@6: animation[k].change = v.change Nenue@6: animation[k].fromAlpha = v.fromAlpha Nenue@6: animation[k].toAlpha = v.toAlpha Nenue@6: animation[k].duration = v.duration Nenue@6: end Nenue@6: end Nenue@6: Nenue@6: function mod:OnEnable() Nenue@6: T:RegisterChatCommand('tkc', CT_Unlock) Nenue@6: Nenue@6: --- Populate CT frames Nenue@6: for name, CT in pairs(mod.CombatTextFrames) do Nenue@6: print('create CT', name) Nenue@6: -- make frame Nenue@6: CT.frame = CT.frame or CreateFrame('Frame', SCT_NAME:format(name), UIParent, SCT_TEMPLATE) Nenue@6: Nenue@6: -- local vars Nenue@6: local db = db[name] or db Nenue@6: local frame = CT.frame Nenue@6: Nenue@6: -- script defs Nenue@6: frame.IsFrameEvent = CT.trigger Nenue@6: frame.name = name Nenue@6: frame.lineID = 0 Nenue@6: frame.expired = {} Nenue@6: frame.active = {} Nenue@6: Nenue@6: -- frame defs Nenue@6: frame:SetPoint(db.anchor, db.parent, db.anchorTo, db.x, db.y) Nenue@6: frame:RegisterEvent('COMBAT_LOG_EVENT_UNFILTERED') Nenue@6: print('bound ', name, 'with', CT.OnEvent) Nenue@6: db.sample = {} Nenue@6: local logged = {} Nenue@6: frame:SetScript('OnEvent', function(self, e,...) Nenue@6: if CT.trigger(e, ...) then Nenue@6: print('event trigger fired', name) Nenue@6: if #db.sample < 5 and not logged[select(2,...)] then Nenue@6: logged[select(2,...)] = true Nenue@6: tinsert(db.sample, {e, ...}) Nenue@6: end Nenue@6: Nenue@6: CT.OnEvent(frame, e, ...) Nenue@6: end Nenue@6: end) Nenue@6: Nenue@6: -- configurators Nenue@6: frame.configHeader:SetText(name) Nenue@6: frame:EnableMouse(false) Nenue@6: Nenue@6: -- pre-pop some text frames Nenue@6: for i = (CT.lineID or 1), SCT_PREGAME do Nenue@6: print(frame:GetName(), 'pre-pop #'..i) Nenue@6: mod.NewCombatMessage(frame) Nenue@6: end Nenue@6: end Nenue@6: end Nenue@6: Nenue@6: Nenue@6: local damageEvents = { Nenue@6: ['SPELL_DAMAGE'] = true, Nenue@6: ['SPELL_PERIODIC_DAMAGE'] = true, Nenue@6: ['SWING_DAMAGE'] = true, Nenue@6: ['RANGE_DAMAGE'] = true, Nenue@6: ['SPELL_HEAL'] = true, Nenue@6: } Nenue@6: mod.CombatTextFrames = { Nenue@6: Incoming = { Nenue@6: trigger = function(e, _, c, _, _, _, _, _, d) return (d == T.GUID and damageEvents[c]) end, Nenue@6: OnEvent = mod.OnDamage, Nenue@6: sample = { Nenue@6: {"COMBAT_LOG_EVENT_UNFILTERED", 1455887795.271, "SPELL_HEAL", false, "Player-3684-07235A4E", "Klakyn", 1297, 0, "Player-3684-07235A4E", "Klakyn", 1297, 0, 143924, "Leech", 1, 93, 93, 0, false, false, }, Nenue@6: } Nenue@6: }, Nenue@6: Outgoing = { Nenue@6: trigger = function(e, _,c,_, s) return (s == T.GUID and damageEvents[c]) end, Nenue@6: OnEvent = mod.OnDamage, Nenue@6: sample = { Nenue@6: { Nenue@6: "COMBAT_LOG_EVENT_UNFILTERED", 1455887795.006, "SPELL_DAMAGE", false, "Player-3684-07235A4E", "Klakyn", 1297, 0, "Creature-0-3684-1116-7-87761-0000C32119", "Dungeoneer's Training Dummy", 68136, 0, 589, "Shadow Word: Pain", 32, 3944, -1, 32, nil, nil, nil, false, false, false, false, false, }, { Nenue@6: "COMBAT_LOG_EVENT_UNFILTERED", 1455887795.271, "SPELL_HEAL", false, "Player-3684-07235A4E", "Klakyn", 1297, 0, "Player-3684-07235A4E", "Klakyn", 1297, 0, 143924, "Leech", 1, 93, 93, 0, false, false, }, { Nenue@6: "COMBAT_LOG_EVENT_UNFILTERED", 1455887797.377, "SPELL_PERIODIC_DAMAGE", false, "Player-3684-07235A4E", "Klakyn", 1297, 0, "Creature-0-3684-1116-7-87761-0000C32119", "Dungeoneer's Training Dummy", 68136, 0, 589, "Shadow Word: Pain", 32, 3944, -1, 32, nil, nil, nil, false, false, false, false, false, }, { Nenue@6: "COMBAT_LOG_EVENT_UNFILTERED", 1455887812.702, "SWING_DAMAGE", false, "Player-3684-07235A4E", "Klakyn", 1297, 0, "Creature-0-3684-1116-7-87761-0000C32119", "Dungeoneer's Training Dummy", 68136, 0, 279, -1, 1, nil, 120, nil, false, false, false, false, false, }, -- [4] Nenue@6: } Nenue@6: }, Nenue@6: DoTTracker = { Nenue@6: trigger = function(e, _, c, _, s) return (s == T.GUID and dotEvents[c]) end, Nenue@6: OnEvent = mod.OnDotEvent, Nenue@6: sample = { Nenue@6: {"COMBAT_LOG_EVENT_UNFILTERED", 1455887795.006, "SPELL_AURA_APPLIED", false, Nenue@6: "Player-3684-07235A4E", "Klakyn", 1297, 0, Nenue@6: "Creature-0-3684-1116-7-87761-0000C32119", "Dungeoneer's Training Dummy", 68136, 0, Nenue@6: 589, "Shadow Word: Pain", 32, "DEBUFF", }, Nenue@6: {"COMBAT_LOG_EVENT_UNFILTERED", 1455887797.377, "SPELL_PERIODIC_DAMAGE", false, Nenue@6: "Player-3684-07235A4E", "Klakyn", 1297, 0, Nenue@6: "Creature-0-3684-1116-7-87761-0000C32119", "Dungeoneer's Training Dummy", 68136, 0, Nenue@6: 589, "Shadow Word: Pain", 32, 3944, -1, 32, nil, nil, nil, false, false, false, false, false, }, Nenue@6: {"COMBAT_LOG_EVENT_UNFILTERED", 1455887807.199, "SPELL_AURA_REMOVED", false, Nenue@6: "Player-3684-07235A4E", "Klakyn", 1297, 0, Nenue@6: "Player-3684-07235A4E", "Klakyn", 1297, 0, Nenue@6: 15473, "Shadowform", 32, "BUFF", }, Nenue@6: } Nenue@6: } Nenue@6: }