2815 lines
101 KiB
Lua
2815 lines
101 KiB
Lua
local ADDON_NAME = "HailMaryGuildTools"
|
|
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
|
if not HMGT then return end
|
|
|
|
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME, true) or {}
|
|
local LSM = LibStub("LibSharedMedia-3.0", true)
|
|
local AceGUI = LibStub("AceGUI-3.0", true)
|
|
|
|
local RT = HMGT:NewModule("RaidTimeline")
|
|
HMGT.RaidTimeline = RT
|
|
|
|
RT.runtimeEnabled = false
|
|
RT.eventFrame = nil
|
|
RT.ticker = nil
|
|
RT.alertFrame = nil
|
|
RT.activeEncounterId = nil
|
|
RT.activeEncounterStartTime = nil
|
|
RT.activeSchedule = nil
|
|
RT.firedEntries = nil
|
|
RT.currentAlert = nil
|
|
RT.receivedAssignments = nil
|
|
RT.timelineEditor = nil
|
|
RT.timelineEditorState = nil
|
|
RT.activeEncounterDifficulty = nil
|
|
RT.previewAlertActive = false
|
|
RT.activeBossAbilityDispatches = nil
|
|
RT.activeBossAbilityCounts = nil
|
|
RT.testActive = false
|
|
RT.testEncounterId = nil
|
|
RT.testDuration = nil
|
|
RT.dispatchedEntries = nil
|
|
|
|
local encounterInstanceInfoCache = {}
|
|
local encounterDirtyFlags = {}
|
|
local encounterNormalizedFlags = {}
|
|
local bossAbilityDefinitionsCache = {}
|
|
|
|
local ENCOUNTER_DIFFICULTY_DEFAULTS = {
|
|
lfr = true,
|
|
normal = true,
|
|
heroic = true,
|
|
mythic = true,
|
|
}
|
|
|
|
local function Clamp(value, minValue, maxValue, fallback)
|
|
local num = tonumber(value)
|
|
if not num then
|
|
num = tonumber(fallback) or minValue
|
|
end
|
|
if num < minValue then num = minValue end
|
|
if num > maxValue then num = maxValue end
|
|
return num
|
|
end
|
|
|
|
local function NormalizeName(name)
|
|
return HMGT.NormalizePlayerName and HMGT:NormalizePlayerName(name) or name
|
|
end
|
|
|
|
local function GetOwnName()
|
|
return NormalizeName(UnitName("player"))
|
|
end
|
|
|
|
local function GetSpellName(spellId)
|
|
local sid = tonumber(spellId)
|
|
if not sid or sid <= 0 then return nil end
|
|
if C_Spell and type(C_Spell.GetSpellName) == "function" then
|
|
local name = C_Spell.GetSpellName(sid)
|
|
if type(name) == "string" and name ~= "" then
|
|
return name
|
|
end
|
|
end
|
|
if type(GetSpellInfo) == "function" then
|
|
local name = GetSpellInfo(sid)
|
|
if type(name) == "string" and name ~= "" then
|
|
return name
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function StableHash(...)
|
|
local hash = 0
|
|
for argIndex = 1, select("#", ...) do
|
|
local value = tostring(select(argIndex, ...) or "")
|
|
for i = 1, #value do
|
|
hash = (hash * 33 + string.byte(value, i)) % 2147483647
|
|
end
|
|
end
|
|
return hash
|
|
end
|
|
|
|
local function GetTimelineMessagePrefix()
|
|
return HMGT.MSG_RAID_TIMELINE or "RTL"
|
|
end
|
|
|
|
local function GetTimelineTestMessagePrefix()
|
|
return HMGT.MSG_RAID_TIMELINE_TEST or "RTS"
|
|
end
|
|
|
|
local function NormalizeEncounterDifficulties(difficulties)
|
|
local source = type(difficulties) == "table" and difficulties or {}
|
|
return {
|
|
lfr = source.lfr ~= false,
|
|
normal = source.normal ~= false,
|
|
heroic = source.heroic ~= false,
|
|
mythic = source.mythic ~= false,
|
|
}
|
|
end
|
|
|
|
local function GetDifficultyKey(difficultyId)
|
|
local id = tonumber(difficultyId)
|
|
if id == 17 then
|
|
return "lfr"
|
|
elseif id == 14 then
|
|
return "normal"
|
|
elseif id == 15 then
|
|
return "heroic"
|
|
elseif id == 16 then
|
|
return "mythic"
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local TEXT_REMINDER_ITEM_ID = 134332
|
|
|
|
local function GetTextReminderIcon()
|
|
if C_Item and type(C_Item.GetItemIconByID) == "function" then
|
|
local icon = C_Item.GetItemIconByID(TEXT_REMINDER_ITEM_ID)
|
|
if icon and icon ~= 0 and icon ~= "" then
|
|
return icon
|
|
end
|
|
end
|
|
if type(GetItemIcon) == "function" then
|
|
local icon = GetItemIcon(TEXT_REMINDER_ITEM_ID)
|
|
if icon and icon ~= 0 and icon ~= "" then
|
|
return icon
|
|
end
|
|
end
|
|
return 136243
|
|
end
|
|
|
|
local function GetPreferredTestDifficultyId(encounter)
|
|
local difficulties = encounter and encounter.difficulties or {}
|
|
if difficulties.normal ~= false then
|
|
return 14
|
|
elseif difficulties.heroic ~= false then
|
|
return 15
|
|
elseif difficulties.mythic ~= false then
|
|
return 16
|
|
elseif difficulties.lfr ~= false then
|
|
return 17
|
|
end
|
|
return 14
|
|
end
|
|
|
|
local function NormalizeEntryType(entryType, spellId)
|
|
local kind = tostring(entryType or (((tonumber(spellId) or 0) > 0) and "spell" or "text"))
|
|
if kind == "text" then
|
|
return "text"
|
|
elseif kind == "bossAbility" then
|
|
return "bossAbility"
|
|
end
|
|
return "spell"
|
|
end
|
|
|
|
local function NormalizeLegacyEntry(triggerType, actionType, entryType, spellId)
|
|
local legacyType = NormalizeEntryType(entryType, spellId)
|
|
local trigger = tostring(triggerType or "")
|
|
local action = tostring(actionType or "")
|
|
if trigger == "" or action == "" then
|
|
if legacyType == "spell" then
|
|
trigger = "time"
|
|
action = "raidCooldown"
|
|
elseif legacyType == "text" then
|
|
trigger = "time"
|
|
action = "text"
|
|
elseif legacyType == "bossAbility" then
|
|
trigger = "bossAbility"
|
|
action = ((tonumber(spellId) or 0) > 0) and "raidCooldown" or "text"
|
|
end
|
|
end
|
|
if trigger ~= "bossAbility" then
|
|
trigger = "time"
|
|
end
|
|
if action ~= "text" then
|
|
action = "raidCooldown"
|
|
end
|
|
return trigger, action
|
|
end
|
|
|
|
local function GetLegacyEntryTypeFromAxes(triggerType, actionType)
|
|
if tostring(triggerType or "") == "bossAbility" then
|
|
return "bossAbility"
|
|
end
|
|
if tostring(actionType or "") == "text" then
|
|
return "text"
|
|
end
|
|
return "spell"
|
|
end
|
|
|
|
local function TrimText(value)
|
|
return tostring(value or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
|
end
|
|
|
|
local function NormalizeBossAbilityBarName(value)
|
|
local text = TrimText(value)
|
|
if text == "" then
|
|
return ""
|
|
end
|
|
text = text:gsub("%s*%(%d+%)%s*$", "")
|
|
text = text:gsub("%s*%([Cc]ount%)%s*$", "")
|
|
return TrimText(text)
|
|
end
|
|
|
|
local function NormalizeCastCountSelector(value)
|
|
local text = string.lower(TrimText(value))
|
|
if text == "" then
|
|
return "1"
|
|
end
|
|
if text == "all" or text == "odd" or text == "even" then
|
|
return text
|
|
end
|
|
local numeric = tonumber(text)
|
|
if numeric and numeric >= 1 then
|
|
return tostring(math.max(1, math.floor(numeric + 0.5)))
|
|
end
|
|
return "1"
|
|
end
|
|
|
|
local function FormatCastCountSelector(value)
|
|
local selector = NormalizeCastCountSelector(value)
|
|
if selector == "all" then
|
|
return L["OPT_RT_CAST_ALL"] or "All"
|
|
end
|
|
if selector == "odd" then
|
|
return L["OPT_RT_CAST_ODD"] or "Odd"
|
|
end
|
|
if selector == "even" then
|
|
return L["OPT_RT_CAST_EVEN"] or "Even"
|
|
end
|
|
return selector
|
|
end
|
|
|
|
local function DoesCastCountSelectorMatch(selector, currentCount)
|
|
local normalizedSelector = NormalizeCastCountSelector(selector)
|
|
local observedCount = math.max(1, tonumber(currentCount) or 1)
|
|
if normalizedSelector == "all" then
|
|
return true
|
|
end
|
|
if normalizedSelector == "odd" then
|
|
return (observedCount % 2) == 1
|
|
end
|
|
if normalizedSelector == "even" then
|
|
return (observedCount % 2) == 0
|
|
end
|
|
return observedCount == math.max(1, tonumber(normalizedSelector) or 1)
|
|
end
|
|
|
|
local function ParseTimelineClock(value)
|
|
local text = TrimText(value)
|
|
if text == "" then
|
|
return nil
|
|
end
|
|
local minutes, seconds = text:match("^(%d+)%s*:%s*(%d%d)$")
|
|
if minutes and seconds then
|
|
local mm = tonumber(minutes)
|
|
local ss = tonumber(seconds)
|
|
if mm and ss and ss >= 0 and ss <= 59 then
|
|
return (mm * 60) + ss
|
|
end
|
|
end
|
|
local numeric = tonumber(text)
|
|
if numeric and numeric >= 0 then
|
|
return numeric
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function NormalizeInstanceName(name)
|
|
local text = TrimText(name)
|
|
if text == "" then
|
|
text = L["OPT_RT_RAID_DEFAULT"] or "Encounter"
|
|
end
|
|
return text
|
|
end
|
|
|
|
local function EnsureEncounterJournalReady()
|
|
if type(EncounterJournal_LoadUI) == "function" then
|
|
pcall(EncounterJournal_LoadUI)
|
|
return true
|
|
end
|
|
if C_AddOns and type(C_AddOns.LoadAddOn) == "function" then
|
|
pcall(C_AddOns.LoadAddOn, "Blizzard_EncounterJournal")
|
|
return type(EJ_GetEncounterInfo) == "function"
|
|
end
|
|
if type(LoadAddOn) == "function" then
|
|
pcall(LoadAddOn, "Blizzard_EncounterJournal")
|
|
return type(EJ_GetEncounterInfo) == "function"
|
|
end
|
|
return false
|
|
end
|
|
|
|
function RT:NormalizeCastCountSelector(value)
|
|
return NormalizeCastCountSelector(value)
|
|
end
|
|
|
|
function RT:FormatCastCountSelector(value)
|
|
return FormatCastCountSelector(value)
|
|
end
|
|
|
|
function RT:DoesCastCountSelectorMatch(selector, currentCount)
|
|
return DoesCastCountSelectorMatch(selector, currentCount)
|
|
end
|
|
|
|
local function ResolveEncounterInstanceByJournalScan(encounterId)
|
|
EnsureEncounterJournalReady()
|
|
if type(EJ_GetNumTiers) ~= "function"
|
|
or type(EJ_SelectTier) ~= "function"
|
|
or type(EJ_GetInstanceByIndex) ~= "function"
|
|
or type(EJ_GetEncounterInfoByIndex) ~= "function"
|
|
then
|
|
return nil, nil
|
|
end
|
|
|
|
local targetEncounterId = tonumber(encounterId)
|
|
if not targetEncounterId or targetEncounterId <= 0 then
|
|
return nil, nil
|
|
end
|
|
|
|
local numTiers = tonumber(EJ_GetNumTiers()) or 0
|
|
for tierIndex = 1, numTiers do
|
|
pcall(EJ_SelectTier, tierIndex)
|
|
for instanceIndex = 1, 200 do
|
|
local instanceId, instanceName = EJ_GetInstanceByIndex(instanceIndex, true)
|
|
instanceId = tonumber(instanceId)
|
|
if not instanceId or instanceId <= 0 then
|
|
break
|
|
end
|
|
|
|
for encounterIndex = 1, 200 do
|
|
local _, _, journalEncounterId, _, _, _, dungeonEncounterId = EJ_GetEncounterInfoByIndex(encounterIndex, instanceId)
|
|
journalEncounterId = tonumber(journalEncounterId)
|
|
dungeonEncounterId = tonumber(dungeonEncounterId)
|
|
if (not journalEncounterId or journalEncounterId <= 0) and (not dungeonEncounterId or dungeonEncounterId <= 0) then
|
|
break
|
|
end
|
|
if journalEncounterId == targetEncounterId or dungeonEncounterId == targetEncounterId then
|
|
return instanceId, NormalizeInstanceName(instanceName)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return nil, nil
|
|
end
|
|
|
|
function RT:GetEncounterInstanceInfo(encounterId)
|
|
local encounter = tonumber(encounterId)
|
|
if not encounter or encounter <= 0 then
|
|
return nil, NormalizeInstanceName(nil)
|
|
end
|
|
|
|
local cached = encounterInstanceInfoCache[encounter]
|
|
if cached then
|
|
return cached.journalInstanceId, cached.instanceName
|
|
end
|
|
|
|
EnsureEncounterJournalReady()
|
|
|
|
local journalInstanceId
|
|
if type(EJ_GetEncounterInfo) == "function" then
|
|
local ok = pcall(function()
|
|
journalInstanceId = select(6, EJ_GetEncounterInfo(encounter))
|
|
end)
|
|
if not ok then
|
|
journalInstanceId = nil
|
|
end
|
|
end
|
|
|
|
journalInstanceId = tonumber(journalInstanceId) or 0
|
|
local instanceName
|
|
if journalInstanceId > 0 and type(EJ_GetInstanceInfo) == "function" then
|
|
local ok = pcall(function()
|
|
instanceName = EJ_GetInstanceInfo(journalInstanceId)
|
|
end)
|
|
if not ok then
|
|
instanceName = nil
|
|
end
|
|
end
|
|
|
|
if (journalInstanceId <= 0 or not instanceName or instanceName == "") then
|
|
local fallbackInstanceId, fallbackInstanceName = ResolveEncounterInstanceByJournalScan(encounter)
|
|
if fallbackInstanceId and fallbackInstanceId > 0 then
|
|
journalInstanceId = fallbackInstanceId
|
|
end
|
|
if fallbackInstanceName and fallbackInstanceName ~= "" then
|
|
instanceName = fallbackInstanceName
|
|
end
|
|
end
|
|
|
|
local resolvedId = (journalInstanceId > 0 and journalInstanceId or nil)
|
|
local resolvedName = NormalizeInstanceName(instanceName)
|
|
encounterInstanceInfoCache[encounter] = {
|
|
journalInstanceId = resolvedId,
|
|
instanceName = resolvedName,
|
|
}
|
|
return resolvedId, resolvedName
|
|
end
|
|
|
|
local function GetBossAbilityDataRoot()
|
|
local root = HMGT.RaidTimelineBossAbilityData
|
|
if type(root) ~= "table" then
|
|
return nil
|
|
end
|
|
if type(root.raids) ~= "table" then
|
|
root.raids = {}
|
|
end
|
|
return root
|
|
end
|
|
|
|
local function NormalizeBossAbilityDifficulties(difficulties)
|
|
local source = type(difficulties) == "table" and difficulties or {}
|
|
return {
|
|
lfr = source.lfr == true,
|
|
normal = source.normal == true,
|
|
heroic = source.heroic == true,
|
|
mythic = source.mythic == true,
|
|
}
|
|
end
|
|
|
|
local function GetBossAbilityDifficultySuffix(definition)
|
|
local difficulties = NormalizeBossAbilityDifficulties(definition and definition.difficulties)
|
|
local enabled = {}
|
|
if difficulties.lfr then enabled[#enabled + 1] = "LFR" end
|
|
if difficulties.normal then enabled[#enabled + 1] = "N" end
|
|
if difficulties.heroic then enabled[#enabled + 1] = "HC" end
|
|
if difficulties.mythic then enabled[#enabled + 1] = "M" end
|
|
if #enabled == 0 or #enabled == 4 then
|
|
return ""
|
|
end
|
|
return string.format(" [%s]", table.concat(enabled, "/"))
|
|
end
|
|
|
|
function RT:GetBossAbilityDefinitions(encounterId)
|
|
local targetEncounterId = tonumber(encounterId)
|
|
local cached = bossAbilityDefinitionsCache[targetEncounterId or 0]
|
|
if cached then
|
|
return cached
|
|
end
|
|
|
|
local root = GetBossAbilityDataRoot()
|
|
local definitions = {}
|
|
if not root or not targetEncounterId or targetEncounterId <= 0 then
|
|
return definitions
|
|
end
|
|
|
|
for _, raid in ipairs(root.raids or {}) do
|
|
local bosses = type(raid) == "table" and raid.bosses or nil
|
|
local boss = bosses and bosses[targetEncounterId]
|
|
if type(boss) == "table" and type(boss.abilities) == "table" then
|
|
for _, ability in ipairs(boss.abilities) do
|
|
if type(ability) == "table" then
|
|
local key = TrimText(ability.key or "")
|
|
if key ~= "" then
|
|
definitions[#definitions + 1] = {
|
|
key = key,
|
|
name = TrimText(ability.name or ""),
|
|
spellId = tonumber(ability.spellId) or 0,
|
|
icon = ability.icon,
|
|
difficulties = ability.difficulties,
|
|
triggers = ability.triggers,
|
|
encounterId = targetEncounterId,
|
|
raidName = TrimText(raid.name or ""),
|
|
bossName = TrimText(boss.name or ""),
|
|
}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
table.sort(definitions, function(a, b)
|
|
local aName = a.name ~= "" and a.name or tostring(a.key or "")
|
|
local bName = b.name ~= "" and b.name or tostring(b.key or "")
|
|
return aName < bName
|
|
end)
|
|
bossAbilityDefinitionsCache[targetEncounterId] = definitions
|
|
return definitions
|
|
end
|
|
|
|
function RT:MarkEncounterDirty(encounterId)
|
|
local id = tonumber(encounterId)
|
|
if id and id > 0 then
|
|
encounterDirtyFlags[id] = true
|
|
encounterNormalizedFlags[id] = nil
|
|
end
|
|
end
|
|
|
|
function RT:GetBossAbilityDefinition(encounterId, abilityKey)
|
|
local key = TrimText(abilityKey or "")
|
|
if key == "" then
|
|
return nil
|
|
end
|
|
for _, definition in ipairs(self:GetBossAbilityDefinitions(encounterId)) do
|
|
if tostring(definition.key or "") == key then
|
|
return definition
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function RT:GetBossAbilityDisplayText(definition)
|
|
if type(definition) ~= "table" then
|
|
return L["OPT_RT_NO_BOSS_ABILITY"] or "No boss ability"
|
|
end
|
|
local text = TrimText(definition.name or "")
|
|
local spellId = tonumber(definition.spellId) or 0
|
|
if text == "" then
|
|
if spellId > 0 then
|
|
text = GetSpellName(spellId) or ("Spell " .. tostring(spellId))
|
|
else
|
|
text = tostring(definition.key or (L["OPT_RT_NO_BOSS_ABILITY"] or "No boss ability"))
|
|
end
|
|
end
|
|
text = text .. GetBossAbilityDifficultySuffix(definition)
|
|
local icon = definition.icon
|
|
if (not icon or icon == "") and spellId > 0 and HMGT_SpellData and type(HMGT_SpellData.GetSpellIcon) == "function" then
|
|
icon = HMGT_SpellData.GetSpellIcon(spellId)
|
|
end
|
|
if (not icon or icon == "") and spellId > 0 and C_Spell and type(C_Spell.GetSpellTexture) == "function" then
|
|
icon = C_Spell.GetSpellTexture(spellId)
|
|
end
|
|
if icon and icon ~= "" then
|
|
return string.format("|T%s:16:16:0:0|t %s", tostring(icon), text)
|
|
end
|
|
return text
|
|
end
|
|
|
|
function RT:EncodeCommText(value)
|
|
local text = tostring(value or "")
|
|
text = text:gsub("%%", "%%25")
|
|
text = text:gsub("|", "%%7C")
|
|
text = text:gsub("\n", "%%0A")
|
|
text = text:gsub("\r", "")
|
|
return text
|
|
end
|
|
|
|
function RT:DecodeCommText(value)
|
|
local text = tostring(value or "")
|
|
text = text:gsub("%%0A", "\n")
|
|
text = text:gsub("%%7C", "|")
|
|
text = text:gsub("%%25", "%%")
|
|
return text
|
|
end
|
|
|
|
function RT:GetSettings()
|
|
local profile = HMGT.db and HMGT.db.profile
|
|
if not profile then
|
|
return nil
|
|
end
|
|
profile.raidTimeline = profile.raidTimeline or {}
|
|
local settings = profile.raidTimeline
|
|
if settings.enabled == nil then settings.enabled = false end
|
|
settings.leadTime = math.floor(Clamp(settings.leadTime, 1, 15, 5) + 0.5)
|
|
settings.assignmentLeadTime = math.floor(Clamp(settings.assignmentLeadTime, 0, 60, settings.leadTime or 5) + 0.5)
|
|
settings.unlocked = settings.unlocked == true
|
|
settings.alertPosX = Clamp(settings.alertPosX, -2000, 2000, 0)
|
|
settings.alertPosY = Clamp(settings.alertPosY, -2000, 2000, 180)
|
|
settings.alertFont = tostring(settings.alertFont or "Friz Quadrata TT")
|
|
settings.alertFontSize = math.floor(Clamp(settings.alertFontSize, 10, 72, 30) + 0.5)
|
|
settings.alertFontOutline = tostring(settings.alertFontOutline or "OUTLINE")
|
|
settings.alertColor = type(settings.alertColor) == "table" and settings.alertColor or { r = 1, g = 0.82, b = 0.15, a = 1 }
|
|
settings.alertColor.r = tonumber(settings.alertColor.r or settings.alertColor[1]) or 1
|
|
settings.alertColor.g = tonumber(settings.alertColor.g or settings.alertColor[2]) or 0.82
|
|
settings.alertColor.b = tonumber(settings.alertColor.b or settings.alertColor[3]) or 0.15
|
|
settings.alertColor.a = tonumber(settings.alertColor.a or settings.alertColor[4]) or 1
|
|
settings.encounters = type(settings.encounters) == "table" and settings.encounters or {}
|
|
return settings
|
|
end
|
|
|
|
function RT:GetEncounterIds()
|
|
local ids = {}
|
|
local encounters = (self:GetSettings() or {}).encounters or {}
|
|
for encounterId in pairs(encounters) do
|
|
local id = tonumber(encounterId)
|
|
if id and id > 0 then
|
|
ids[#ids + 1] = id
|
|
end
|
|
end
|
|
table.sort(ids)
|
|
return ids
|
|
end
|
|
|
|
function RT:GetEncounter(encounterId)
|
|
local id = tonumber(encounterId)
|
|
if not id or id <= 0 then
|
|
return nil
|
|
end
|
|
local settings = self:GetSettings()
|
|
local encounter = settings and settings.encounters and settings.encounters[id]
|
|
if type(encounter) ~= "table" then
|
|
return nil
|
|
end
|
|
if encounterDirtyFlags[id] ~= true and encounterNormalizedFlags[id] == true then
|
|
return encounter
|
|
end
|
|
local journalInstanceId, instanceName = self:GetEncounterInstanceInfo(id)
|
|
encounter.name = tostring(encounter.name or "")
|
|
encounter.journalInstanceId = journalInstanceId or tonumber(encounter.journalInstanceId) or 0
|
|
encounter.instanceName = instanceName
|
|
encounter.difficulties = NormalizeEncounterDifficulties(encounter.difficulties)
|
|
encounter.entries = type(encounter.entries) == "table" and encounter.entries or {}
|
|
for _, entry in ipairs(encounter.entries) do
|
|
entry.time = math.max(0, math.floor((ParseTimelineClock(entry.time) or tonumber(entry.time) or 0) + 0.5))
|
|
entry.spellId = math.max(0, tonumber(entry.spellId) or 0)
|
|
entry.playerName = TrimText(entry.playerName or "")
|
|
entry.entryType = NormalizeEntryType(entry.entryType, entry.spellId)
|
|
entry.triggerType, entry.actionType = NormalizeLegacyEntry(entry.triggerType, entry.actionType, entry.entryType, entry.spellId)
|
|
entry.entryType = GetLegacyEntryTypeFromAxes(entry.triggerType, entry.actionType)
|
|
entry.bossAbilityId = TrimText(entry.bossAbilityId or "")
|
|
entry.bossAbilityBarName = NormalizeBossAbilityBarName(entry.bossAbilityBarName or "")
|
|
entry.castCount = NormalizeCastCountSelector(entry.castCount)
|
|
entry.targetSpec = TrimText(entry.targetSpec or ((entry.actionType == "text") and entry.playerName or ""))
|
|
entry.alertText = tostring(entry.alertText or "")
|
|
if entry.actionType == "text" then
|
|
entry.spellId = 0
|
|
end
|
|
if entry.triggerType ~= "bossAbility" then
|
|
entry.bossAbilityId = ""
|
|
entry.bossAbilityBarName = ""
|
|
entry.castCount = "1"
|
|
else
|
|
entry.time = 0
|
|
end
|
|
end
|
|
table.sort(encounter.entries, function(a, b)
|
|
local aTime = tonumber(a and a.time) or 0
|
|
local bTime = tonumber(b and b.time) or 0
|
|
if aTime ~= bTime then
|
|
return aTime < bTime
|
|
end
|
|
local aSpell = tonumber(a and a.spellId) or 0
|
|
local bSpell = tonumber(b and b.spellId) or 0
|
|
if aSpell ~= bSpell then
|
|
return aSpell < bSpell
|
|
end
|
|
return tostring(a and a.playerName or "") < tostring(b and b.playerName or "")
|
|
end)
|
|
encounterDirtyFlags[id] = nil
|
|
encounterNormalizedFlags[id] = true
|
|
return encounter
|
|
end
|
|
|
|
function RT:EnsureEncounter(encounterId)
|
|
local id = tonumber(encounterId)
|
|
if not id or id <= 0 then
|
|
return nil
|
|
end
|
|
local settings = self:GetSettings()
|
|
settings.encounters[id] = settings.encounters[id] or {
|
|
name = "",
|
|
journalInstanceId = 0,
|
|
instanceName = NormalizeInstanceName(nil),
|
|
difficulties = NormalizeEncounterDifficulties(ENCOUNTER_DIFFICULTY_DEFAULTS),
|
|
entries = {},
|
|
}
|
|
self:MarkEncounterDirty(id)
|
|
return self:GetEncounter(id)
|
|
end
|
|
|
|
function RT:AddEncounter(encounterId, encounterName)
|
|
local id = tonumber(encounterId)
|
|
if not id or id <= 0 then
|
|
return false
|
|
end
|
|
local encounter = self:EnsureEncounter(id)
|
|
encounter.name = tostring(encounterName or encounter.name or "")
|
|
local journalInstanceId, instanceName = self:GetEncounterInstanceInfo(id)
|
|
encounter.journalInstanceId = journalInstanceId or 0
|
|
encounter.instanceName = instanceName
|
|
self:MarkEncounterDirty(id)
|
|
return true
|
|
end
|
|
|
|
function RT:IsEncounterEnabledForDifficulty(encounter, difficultyId)
|
|
if type(encounter) ~= "table" then
|
|
return false
|
|
end
|
|
local key = GetDifficultyKey(difficultyId)
|
|
if not key then
|
|
return true
|
|
end
|
|
local difficulties = NormalizeEncounterDifficulties(encounter.difficulties)
|
|
encounter.difficulties = difficulties
|
|
return difficulties[key] == true
|
|
end
|
|
|
|
function RT:RemoveEncounter(encounterId)
|
|
local id = tonumber(encounterId)
|
|
local settings = self:GetSettings()
|
|
if not id or not settings or not settings.encounters[id] then
|
|
return false
|
|
end
|
|
settings.encounters[id] = nil
|
|
if self.activeEncounterId == id then
|
|
self:StopEncounter()
|
|
end
|
|
return true
|
|
end
|
|
|
|
function RT:AddEntry(encounterId, timeSec, spellId, playerName, alertText, entryType, targetSpec)
|
|
local encounter = self:EnsureEncounter(encounterId)
|
|
local atTime = ParseTimelineClock(timeSec)
|
|
local sid = math.max(0, tonumber(spellId) or 0)
|
|
local text = tostring(alertText or "")
|
|
local kind = NormalizeEntryType(entryType, sid)
|
|
local triggerType, actionType = NormalizeLegacyEntry(nil, nil, kind, sid)
|
|
local fixedPlayer = TrimText(playerName or "")
|
|
if kind == "text" then
|
|
sid = 0
|
|
end
|
|
local targets = TrimText(targetSpec or ((kind == "text") and fixedPlayer or ""))
|
|
local trimmedText = TrimText(text)
|
|
if not encounter or not atTime or atTime < 0 then
|
|
return false
|
|
end
|
|
if kind == "spell" and sid <= 0 then
|
|
return false
|
|
end
|
|
if kind == "text" and (trimmedText == "" or targets == "") then
|
|
return false
|
|
end
|
|
encounter.entries[#encounter.entries + 1] = {
|
|
time = math.floor(atTime + 0.5),
|
|
spellId = sid,
|
|
playerName = fixedPlayer,
|
|
entryType = GetLegacyEntryTypeFromAxes(triggerType, actionType),
|
|
triggerType = triggerType,
|
|
actionType = actionType,
|
|
targetSpec = targets,
|
|
alertText = text,
|
|
}
|
|
self:MarkEncounterDirty(encounterId)
|
|
self:GetEncounter(encounterId)
|
|
self:Refresh()
|
|
return true
|
|
end
|
|
|
|
function RT:AddDetailedEntry(encounterId, entryData)
|
|
local data = type(entryData) == "table" and entryData or {}
|
|
local encounter = self:EnsureEncounter(encounterId)
|
|
if not encounter then
|
|
return false
|
|
end
|
|
|
|
local triggerType, actionType = NormalizeLegacyEntry(data.triggerType, data.actionType, data.entryType, data.spellId)
|
|
local timeSec = ParseTimelineClock(data.time)
|
|
local spellId = math.max(0, tonumber(data.spellId) or 0)
|
|
local playerName = TrimText(data.playerName or "")
|
|
local alertText = tostring(data.alertText or "")
|
|
local targetSpec = TrimText(data.targetSpec or "")
|
|
local bossAbilityId = TrimText(data.bossAbilityId or "")
|
|
local bossAbilityBarName = NormalizeBossAbilityBarName(data.bossAbilityBarName or "")
|
|
local castCount = NormalizeCastCountSelector(data.castCount)
|
|
|
|
if triggerType == "time" and actionType == "raidCooldown" then
|
|
if timeSec == nil or timeSec < 0 or spellId <= 0 then
|
|
return false
|
|
end
|
|
elseif triggerType == "time" and actionType == "text" then
|
|
if timeSec == nil or timeSec < 0 or TrimText(alertText) == "" then
|
|
return false
|
|
end
|
|
spellId = 0
|
|
bossAbilityId = ""
|
|
castCount = "1"
|
|
elseif triggerType == "bossAbility" and actionType == "text" then
|
|
if bossAbilityBarName == "" or TrimText(alertText) == "" then
|
|
return false
|
|
end
|
|
spellId = 0
|
|
timeSec = 0
|
|
elseif triggerType == "bossAbility" and actionType == "raidCooldown" then
|
|
if bossAbilityBarName == "" or spellId <= 0 then
|
|
return false
|
|
end
|
|
timeSec = 0
|
|
else
|
|
return false
|
|
end
|
|
|
|
encounter.entries[#encounter.entries + 1] = {
|
|
time = math.floor((timeSec or 0) + 0.5),
|
|
spellId = spellId,
|
|
playerName = playerName,
|
|
entryType = GetLegacyEntryTypeFromAxes(triggerType, actionType),
|
|
triggerType = triggerType,
|
|
actionType = actionType,
|
|
targetSpec = targetSpec,
|
|
alertText = alertText,
|
|
bossAbilityId = bossAbilityId,
|
|
bossAbilityBarName = bossAbilityBarName,
|
|
castCount = castCount,
|
|
}
|
|
self:MarkEncounterDirty(encounterId)
|
|
self:GetEncounter(encounterId)
|
|
self:Refresh()
|
|
return true
|
|
end
|
|
|
|
function RT:RemoveEntry(encounterId, row)
|
|
local encounter = self:GetEncounter(encounterId)
|
|
local index = tonumber(row)
|
|
if not encounter or not index or not encounter.entries[index] then
|
|
return false
|
|
end
|
|
table.remove(encounter.entries, index)
|
|
self:MarkEncounterDirty(encounterId)
|
|
self:Refresh()
|
|
return true
|
|
end
|
|
|
|
function RT:SetEntryField(encounterId, row, field, value)
|
|
local encounter = self:GetEncounter(encounterId)
|
|
local index = tonumber(row)
|
|
if not encounter or not index or not encounter.entries[index] then
|
|
return false
|
|
end
|
|
|
|
local entry = encounter.entries[index]
|
|
if field == "time" then
|
|
local timeValue = ParseTimelineClock(value)
|
|
if not timeValue or timeValue < 0 then
|
|
return false
|
|
end
|
|
entry.time = math.floor(timeValue + 0.5)
|
|
elseif field == "spellId" then
|
|
entry.spellId = math.max(0, tonumber(value) or 0)
|
|
elseif field == "playerName" then
|
|
entry.playerName = TrimText(value or "")
|
|
elseif field == "triggerType" then
|
|
local triggerType = tostring(value or "")
|
|
entry.triggerType = (triggerType == "bossAbility") and "bossAbility" or "time"
|
|
if entry.triggerType ~= "bossAbility" then
|
|
entry.bossAbilityId = ""
|
|
entry.bossAbilityBarName = ""
|
|
entry.castCount = "1"
|
|
else
|
|
entry.time = 0
|
|
end
|
|
entry.entryType = GetLegacyEntryTypeFromAxes(entry.triggerType, entry.actionType)
|
|
elseif field == "actionType" then
|
|
local actionType = tostring(value or "")
|
|
entry.actionType = (actionType == "text") and "text" or "raidCooldown"
|
|
if entry.actionType == "text" then
|
|
entry.spellId = 0
|
|
if TrimText(entry.targetSpec or "") == "" then
|
|
entry.targetSpec = TrimText(entry.playerName or "")
|
|
end
|
|
end
|
|
entry.entryType = GetLegacyEntryTypeFromAxes(entry.triggerType, entry.actionType)
|
|
elseif field == "entryType" then
|
|
entry.entryType = NormalizeEntryType(value, entry.spellId)
|
|
entry.triggerType, entry.actionType = NormalizeLegacyEntry(entry.triggerType, entry.actionType, entry.entryType, entry.spellId)
|
|
entry.entryType = GetLegacyEntryTypeFromAxes(entry.triggerType, entry.actionType)
|
|
elseif field == "targetSpec" then
|
|
entry.targetSpec = TrimText(value or "")
|
|
elseif field == "alertText" then
|
|
entry.alertText = tostring(value or "")
|
|
elseif field == "bossAbilityId" then
|
|
entry.bossAbilityId = TrimText(value or "")
|
|
elseif field == "bossAbilityBarName" then
|
|
entry.bossAbilityBarName = NormalizeBossAbilityBarName(value or "")
|
|
elseif field == "castCount" then
|
|
entry.castCount = NormalizeCastCountSelector(value)
|
|
else
|
|
return false
|
|
end
|
|
|
|
self:MarkEncounterDirty(encounterId)
|
|
self:GetEncounter(encounterId)
|
|
self:Refresh()
|
|
return true
|
|
end
|
|
|
|
function RT:GetSpellEntry(spellId)
|
|
local sid = tonumber(spellId)
|
|
if not sid or sid <= 0 then
|
|
return nil
|
|
end
|
|
return HMGT_SpellData and HMGT_SpellData.CooldownLookup and HMGT_SpellData.CooldownLookup[sid] or nil
|
|
end
|
|
|
|
function RT:GetRaidCooldownSpellValues()
|
|
local values = {
|
|
[0] = L["OPT_RT_NO_SPELL"] or "No spell",
|
|
}
|
|
local seen = { [0] = true }
|
|
local spells = HMGT_SpellData and HMGT_SpellData.RaidCooldowns or {}
|
|
for _, entry in ipairs(spells) do
|
|
local spellId = tonumber(entry and entry.spellId)
|
|
if spellId and spellId > 0 and not seen[spellId] then
|
|
seen[spellId] = true
|
|
local spellName = GetSpellName(spellId) or tostring(entry.name or ("Spell " .. spellId))
|
|
local icon = nil
|
|
if HMGT_SpellData and type(HMGT_SpellData.GetSpellIcon) == "function" then
|
|
icon = HMGT_SpellData.GetSpellIcon(spellId)
|
|
end
|
|
if (not icon or icon == "") and C_Spell and type(C_Spell.GetSpellTexture) == "function" then
|
|
icon = C_Spell.GetSpellTexture(spellId)
|
|
end
|
|
if icon and icon ~= "" then
|
|
values[spellId] = string.format("|T%s:16:16:0:0|t %s (%d)", tostring(icon), spellName, spellId)
|
|
else
|
|
values[spellId] = string.format("%s (%d)", spellName, spellId)
|
|
end
|
|
end
|
|
end
|
|
return values
|
|
end
|
|
|
|
function RT:GetBossAbilityValues(encounterId)
|
|
local values = {
|
|
[""] = L["OPT_RT_NO_BOSS_ABILITY"] or "No boss ability",
|
|
}
|
|
local targetEncounterId = tonumber(encounterId)
|
|
if not targetEncounterId or targetEncounterId <= 0 then
|
|
return values
|
|
end
|
|
for _, definition in ipairs(self:GetBossAbilityDefinitions(targetEncounterId)) do
|
|
values[tostring(definition.key)] = self:GetBossAbilityDisplayText(definition)
|
|
end
|
|
return values
|
|
end
|
|
|
|
function RT:GetEntryType(entry)
|
|
return NormalizeEntryType(entry and entry.entryType, entry and entry.spellId)
|
|
end
|
|
|
|
function RT:GetTriggerType(entry)
|
|
local triggerType, _ = NormalizeLegacyEntry(
|
|
entry and entry.triggerType,
|
|
entry and entry.actionType,
|
|
entry and entry.entryType,
|
|
entry and entry.spellId
|
|
)
|
|
return triggerType
|
|
end
|
|
|
|
function RT:GetActionType(entry)
|
|
local _, actionType = NormalizeLegacyEntry(entry and entry.triggerType, entry and entry.actionType, entry and entry.entryType, entry and entry.spellId)
|
|
return actionType
|
|
end
|
|
|
|
function RT:GetTriggerTypeValues()
|
|
return {
|
|
time = L["OPT_RT_TRIGGER_TIME"] or "Time",
|
|
bossAbility = L["OPT_RT_TRIGGER_BOSS_ABILITY"] or "Boss ability",
|
|
}
|
|
end
|
|
|
|
function RT:GetActionTypeValues()
|
|
return {
|
|
text = L["OPT_RT_ACTION_TEXT"] or "Text",
|
|
raidCooldown = L["OPT_RT_ACTION_RAID_COOLDOWN"] or "Raid Cooldown",
|
|
}
|
|
end
|
|
|
|
function RT:GetEntryTypeValues()
|
|
return {
|
|
spell = L["OPT_RT_TYPE_SPELL"] or "Spell",
|
|
text = L["OPT_RT_TYPE_TEXT"] or "Text",
|
|
bossAbility = L["OPT_RT_TYPE_BOSS_ABILITY"] or "Boss ability",
|
|
}
|
|
end
|
|
|
|
function RT:GetEntryAlertText(entry)
|
|
local text = TrimText(entry and entry.alertText or "")
|
|
if text == "" then
|
|
return nil
|
|
end
|
|
return text
|
|
end
|
|
|
|
function RT:GetEntryDisplayText(entry)
|
|
local triggerType = self:GetTriggerType(entry)
|
|
local actionType = self:GetActionType(entry)
|
|
local alertText = self:GetEntryAlertText(entry)
|
|
if actionType == "text" and triggerType ~= "bossAbility" then
|
|
return alertText or (L["OPT_RT_ASSIGNMENT_NONE"] or "No cooldown selected")
|
|
end
|
|
if triggerType == "bossAbility" then
|
|
local encounterId = tonumber(entry and entry.encounterId) or tonumber(self.activeEncounterId) or 0
|
|
local definition = self:GetBossAbilityDefinition(encounterId, entry and entry.bossAbilityId)
|
|
local bossAbilityText = TrimText(entry and entry.bossAbilityBarName or "")
|
|
if bossAbilityText == "" then
|
|
bossAbilityText = self:GetBossAbilityDisplayText(definition)
|
|
end
|
|
local castCount = self:FormatCastCountSelector(entry and entry.castCount)
|
|
if actionType == "text" and alertText then
|
|
return string.format("%s (%s %s)", alertText, L["OPT_RT_CAST"] or "Cast", castCount)
|
|
elseif actionType == "raidCooldown" then
|
|
local spellId = tonumber(entry and entry.spellId) or 0
|
|
local spellName = spellId > 0 and (GetSpellName(spellId) or ("Spell " .. tostring(spellId))) or (L["OPT_RT_ASSIGNMENT_NONE"] or "No cooldown selected")
|
|
return string.format("%s -> %s (%s %s)", bossAbilityText, spellName, L["OPT_RT_CAST"] or "Cast", castCount)
|
|
end
|
|
return string.format("%s (%s %s)", bossAbilityText, L["OPT_RT_CAST"] or "Cast", castCount)
|
|
end
|
|
if alertText then
|
|
return alertText
|
|
end
|
|
local spellId = tonumber(entry and entry.spellId) or 0
|
|
if spellId > 0 then
|
|
return GetSpellName(spellId) or ("Spell " .. tostring(spellId))
|
|
end
|
|
return L["OPT_RT_ASSIGNMENT_NONE"] or "No cooldown selected"
|
|
end
|
|
|
|
function RT:GetEntryIcon(entry)
|
|
local triggerType = self:GetTriggerType(entry)
|
|
local actionType = self:GetActionType(entry)
|
|
local spellId = tonumber(entry and entry.spellId) or 0
|
|
if actionType == "raidCooldown" and spellId > 0 then
|
|
if HMGT_SpellData and type(HMGT_SpellData.GetSpellIcon) == "function" then
|
|
local icon = HMGT_SpellData.GetSpellIcon(spellId)
|
|
if icon and icon ~= "" then
|
|
return icon
|
|
end
|
|
end
|
|
if C_Spell and type(C_Spell.GetSpellTexture) == "function" then
|
|
local icon = C_Spell.GetSpellTexture(spellId)
|
|
if icon and icon ~= "" then
|
|
return icon
|
|
end
|
|
end
|
|
end
|
|
if triggerType == "bossAbility" then
|
|
local encounterId = tonumber(entry and entry.encounterId) or tonumber(self.activeEncounterId) or 0
|
|
local definition = self:GetBossAbilityDefinition(encounterId, entry and entry.bossAbilityId)
|
|
local icon = definition and definition.icon
|
|
local numericAbilityId = tonumber(definition and definition.spellId) or 0
|
|
if (not icon or icon == "") and numericAbilityId > 0 and C_Spell and type(C_Spell.GetSpellTexture) == "function" then
|
|
icon = C_Spell.GetSpellTexture(numericAbilityId)
|
|
end
|
|
if icon and icon ~= "" then
|
|
return icon
|
|
end
|
|
end
|
|
if actionType == "text" then
|
|
return GetTextReminderIcon()
|
|
end
|
|
return 136243
|
|
end
|
|
|
|
local function FormatTimelineClock(value)
|
|
local total = math.max(0, math.floor((tonumber(value) or 0) + 0.5))
|
|
local minutes = math.floor(total / 60)
|
|
local seconds = total % 60
|
|
return string.format("%02d:%02d", minutes, seconds)
|
|
end
|
|
|
|
function RT:FormatTimelineClock(value)
|
|
return FormatTimelineClock(value)
|
|
end
|
|
|
|
function RT:ParseTimelineClock(value)
|
|
return ParseTimelineClock(value)
|
|
end
|
|
|
|
function RT:GetTimelineEditorState()
|
|
self.timelineEditorState = self.timelineEditorState or {
|
|
encounterId = nil,
|
|
viewportStart = 0,
|
|
viewportDuration = 120,
|
|
selectedIndex = nil,
|
|
}
|
|
local state = self.timelineEditorState
|
|
state.viewportStart = Clamp(state.viewportStart, 0, 540, 0)
|
|
state.viewportDuration = Clamp(state.viewportDuration, 30, 300, 120)
|
|
return state
|
|
end
|
|
|
|
function RT:CloseTimelineEditor()
|
|
if self.timelineEditor and self.timelineEditor.frame then
|
|
self.timelineEditor.frame:Release()
|
|
end
|
|
self.timelineEditor = nil
|
|
end
|
|
|
|
function RT:GetTimelineCanvasTime(canvas, cursorX)
|
|
if not canvas then
|
|
return 0
|
|
end
|
|
local width = math.max(1, canvas:GetWidth() or 1)
|
|
local left = canvas:GetLeft() or 0
|
|
local state = self:GetTimelineEditorState()
|
|
local ratio = (tonumber(cursorX) or left) - left
|
|
ratio = ratio / width
|
|
ratio = Clamp(ratio, 0, 1, 0)
|
|
return math.floor((state.viewportStart + (ratio * state.viewportDuration)) + 0.5)
|
|
end
|
|
|
|
function RT:SelectTimelineEntry(index)
|
|
local state = self:GetTimelineEditorState()
|
|
state.selectedIndex = tonumber(index)
|
|
self:RefreshTimelineEditor()
|
|
end
|
|
|
|
function RT:DeleteSelectedTimelineEntry()
|
|
local state = self:GetTimelineEditorState()
|
|
if not state.encounterId or not state.selectedIndex then
|
|
return
|
|
end
|
|
if self:RemoveEntry(state.encounterId, state.selectedIndex) then
|
|
state.selectedIndex = nil
|
|
self:RefreshTimelineEditor()
|
|
end
|
|
end
|
|
|
|
function RT:CreateTimelineEntryAtCursor(cursorX)
|
|
local state = self:GetTimelineEditorState()
|
|
if not self:IsLocalEditor() or not state.encounterId then
|
|
return
|
|
end
|
|
local values = self:GetRaidCooldownSpellValues()
|
|
local firstSpellId = nil
|
|
for spellId in pairs(values) do
|
|
if tonumber(spellId) and tonumber(spellId) > 0 and (not firstSpellId or spellId < firstSpellId) then
|
|
firstSpellId = spellId
|
|
end
|
|
end
|
|
if not firstSpellId then
|
|
return
|
|
end
|
|
local timeSec = self:GetTimelineCanvasTime(self.timelineEditor and self.timelineEditor.canvas, cursorX)
|
|
if self:AddEntry(state.encounterId, timeSec, firstSpellId, "", "") then
|
|
local encounter = self:GetEncounter(state.encounterId)
|
|
state.selectedIndex = encounter and #encounter.entries or nil
|
|
self:RefreshTimelineEditor()
|
|
end
|
|
end
|
|
|
|
function RT:HandleTimelineMarkerDrag(index, cursorX)
|
|
local state = self:GetTimelineEditorState()
|
|
if not self:IsLocalEditor() or not state.encounterId or not index then
|
|
return
|
|
end
|
|
local encounter = self:GetEncounter(state.encounterId)
|
|
local entry = encounter and encounter.entries and encounter.entries[index]
|
|
if not entry then
|
|
return
|
|
end
|
|
local newTime = self:GetTimelineCanvasTime(self.timelineEditor and self.timelineEditor.canvas, cursorX)
|
|
entry.time = math.max(0, newTime)
|
|
self:GetEncounter(state.encounterId)
|
|
state.selectedIndex = index
|
|
self:RefreshTimelineEditor()
|
|
end
|
|
|
|
function RT:RefreshTimelineEditorDetails()
|
|
local editor = self.timelineEditor
|
|
if not editor then
|
|
return
|
|
end
|
|
local state = self:GetTimelineEditorState()
|
|
local encounter = state.encounterId and self:GetEncounter(state.encounterId) or nil
|
|
local entry = encounter and encounter.entries and encounter.entries[state.selectedIndex or 0] or nil
|
|
local selected = entry ~= nil
|
|
local triggerType = entry and self:GetTriggerType(entry) or "time"
|
|
local actionType = entry and self:GetActionType(entry) or "raidCooldown"
|
|
local editable = selected and self:IsLocalEditor()
|
|
|
|
if editor.selectionHeader then
|
|
local encounterLabel = encounter and (encounter.name ~= "" and encounter.name or (L["OPT_RT_ENCOUNTER"] or "Encounter")) or (L["OPT_RT_ENCOUNTER"] or "Encounter")
|
|
local suffix = ""
|
|
if entry then
|
|
local displayEntry = entry
|
|
if triggerType == "bossAbility" then
|
|
displayEntry = {}
|
|
for key, value in pairs(entry) do
|
|
displayEntry[key] = value
|
|
end
|
|
displayEntry.encounterId = state.encounterId
|
|
suffix = string.format(" - %s", self:GetEntryDisplayText(displayEntry))
|
|
else
|
|
suffix = string.format(" - %s @ %s", self:GetEntryDisplayText(entry), FormatTimelineClock(entry.time))
|
|
end
|
|
end
|
|
local headerText = string.format("%s (%s)%s", encounterLabel, tostring(state.encounterId or "-"), suffix)
|
|
editor.selectionHeader:SetText(headerText)
|
|
if editor.selectionHeader.label then
|
|
local stringHeight = editor.selectionHeader.label.GetStringHeight and editor.selectionHeader.label:GetStringHeight() or 0
|
|
editor.selectionHeader:SetHeight(math.max(42, math.ceil((tonumber(stringHeight) or 0) + 8)))
|
|
end
|
|
end
|
|
if editor.timeValue then
|
|
editor.timeValue:SetDisabled(not editable or triggerType == "bossAbility")
|
|
editor.timeValue:SetText(entry and FormatTimelineClock(entry.time or 0) or "")
|
|
if editor.timeValue.frame then
|
|
editor.timeValue.frame:SetShown(triggerType ~= "bossAbility")
|
|
end
|
|
end
|
|
if editor.triggerTypeValue then
|
|
editor.triggerTypeValue:SetDisabled(not editable)
|
|
editor.triggerTypeValue:SetValue(triggerType)
|
|
end
|
|
if editor.actionTypeValue then
|
|
editor.actionTypeValue:SetDisabled(not editable)
|
|
editor.actionTypeValue:SetValue(actionType)
|
|
end
|
|
if editor.spellValue then
|
|
editor.spellValue:SetDisabled(not (editable and actionType == "raidCooldown"))
|
|
editor.spellValue:SetValue(entry and math.max(0, tonumber(entry.spellId) or 0) or 0)
|
|
if editor.spellValue.frame then
|
|
editor.spellValue.frame:SetShown(actionType == "raidCooldown")
|
|
end
|
|
end
|
|
if editor.castCountValue then
|
|
editor.castCountValue:SetDisabled(not (editable and triggerType == "bossAbility"))
|
|
editor.castCountValue:SetText(entry and self:FormatCastCountSelector(entry.castCount) or "1")
|
|
if editor.castCountValue.frame then
|
|
editor.castCountValue.frame:SetShown(triggerType == "bossAbility")
|
|
end
|
|
end
|
|
if editor.bossAbilityBarNameValue then
|
|
editor.bossAbilityBarNameValue:SetDisabled(not (editable and triggerType == "bossAbility"))
|
|
editor.bossAbilityBarNameValue:SetText((triggerType == "bossAbility" and entry) and tostring(entry.bossAbilityBarName or "") or "")
|
|
if editor.bossAbilityBarNameValue.frame then
|
|
editor.bossAbilityBarNameValue.frame:SetShown(triggerType == "bossAbility")
|
|
end
|
|
end
|
|
if editor.textValue then
|
|
editor.textValue:SetDisabled(not editable or actionType ~= "text")
|
|
if editor.textValue.SetLabel then
|
|
editor.textValue:SetLabel(L["OPT_RT_ENTRY_TEXT"] or "Custom text")
|
|
end
|
|
editor.textValue:SetText((actionType == "text" and entry) and tostring(entry.alertText or "") or "")
|
|
if editor.textValue.frame then
|
|
editor.textValue.frame:SetShown(actionType == "text")
|
|
end
|
|
end
|
|
if editor.targetValue then
|
|
editor.targetValue:SetDisabled(not editable)
|
|
if editor.targetValue.SetLabel then
|
|
editor.targetValue:SetLabel((actionType == "text") and (L["OPT_RT_ENTRY_TARGETS"] or "Targets") or (L["OPT_RT_ENTRY_PLAYER"] or "Fixed player"))
|
|
end
|
|
editor.targetValue:SetText(entry and tostring((actionType == "text") and (entry.targetSpec or "") or (entry.playerName or "")) or "")
|
|
end
|
|
if editor.deleteButton then
|
|
editor.deleteButton:SetDisabled(not editable)
|
|
end
|
|
end
|
|
|
|
function RT:RefreshTimelineEditorCanvas()
|
|
local editor = self.timelineEditor
|
|
if not editor or not editor.canvas then
|
|
return
|
|
end
|
|
|
|
local canvas = editor.canvas
|
|
local state = self:GetTimelineEditorState()
|
|
local encounter = state.encounterId and self:GetEncounter(state.encounterId) or nil
|
|
local entries = encounter and encounter.entries or {}
|
|
local width = math.max(1, canvas:GetWidth() or 1)
|
|
local duration = math.max(30, tonumber(state.viewportDuration) or 120)
|
|
local startSec = Clamp(state.viewportStart, 0, math.max(0, 600 - duration), 0)
|
|
state.viewportStart = startSec
|
|
|
|
if editor.viewportSlider then
|
|
editor.viewportSlider:SetSliderValues(0, math.max(0, 600 - duration), 1)
|
|
editor.viewportSlider:SetValue(startSec)
|
|
end
|
|
if editor.zoomSlider then
|
|
editor.zoomSlider:SetValue(duration)
|
|
end
|
|
if editor.viewportLabel then
|
|
editor.viewportLabel:SetText(string.format("%s - %s", FormatTimelineClock(startSec), FormatTimelineClock(startSec + duration)))
|
|
end
|
|
|
|
editor.markers = editor.markers or {}
|
|
for _, marker in ipairs(editor.markers) do
|
|
marker:Hide()
|
|
end
|
|
|
|
local tickSpacing = 30
|
|
local tickIndex = 1
|
|
for tick = startSec, startSec + duration, tickSpacing do
|
|
local ratio = (tick - startSec) / duration
|
|
local x = math.floor(ratio * (width - 20)) + 10
|
|
local line = editor.tickLines[tickIndex]
|
|
local label = editor.tickLabels[tickIndex]
|
|
if line and label then
|
|
line:ClearAllPoints()
|
|
line:SetPoint("BOTTOMLEFT", canvas, "BOTTOMLEFT", x, 26)
|
|
line:SetSize(1, 28)
|
|
line:Show()
|
|
label:ClearAllPoints()
|
|
label:SetPoint("TOP", line, "BOTTOM", 0, -2)
|
|
label:SetText(FormatTimelineClock(tick))
|
|
label:Show()
|
|
end
|
|
tickIndex = tickIndex + 1
|
|
end
|
|
for i = tickIndex, #editor.tickLines do
|
|
editor.tickLines[i]:Hide()
|
|
editor.tickLabels[i]:Hide()
|
|
end
|
|
|
|
local visibleCount = 0
|
|
for index, entry in ipairs(entries) do
|
|
local timeSec = tonumber(entry.time) or 0
|
|
if self:GetTriggerType(entry) ~= "bossAbility" and timeSec >= startSec and timeSec <= startSec + duration then
|
|
visibleCount = visibleCount + 1
|
|
local marker = editor.markers[visibleCount]
|
|
if not marker then
|
|
marker = CreateFrame("Button", nil, canvas, "BackdropTemplate")
|
|
marker:SetSize(30, 30)
|
|
marker:SetMovable(false)
|
|
marker:RegisterForDrag("LeftButton")
|
|
marker.icon = marker:CreateTexture(nil, "ARTWORK")
|
|
marker.icon:SetAllPoints(marker)
|
|
marker.stem = marker:CreateTexture(nil, "BORDER")
|
|
marker.stem:SetColorTexture(1, 0.82, 0.15, 0.9)
|
|
marker.stem:SetWidth(2)
|
|
marker.border = marker:CreateTexture(nil, "OVERLAY")
|
|
marker.border:SetTexture("Interface\\Buttons\\UI-Debuff-Overlays")
|
|
marker.border:SetTexCoord(.296875, .5703125, 0, .515625)
|
|
marker.border:SetAllPoints(marker)
|
|
marker.label = marker:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
|
marker.label:SetPoint("TOP", marker, "BOTTOM", 0, -2)
|
|
marker.label:SetTextColor(1, 0.82, 0.15)
|
|
marker:SetScript("OnClick", function(button)
|
|
RT:SelectTimelineEntry(button.entryIndex)
|
|
end)
|
|
marker:SetScript("OnEnter", function(button)
|
|
local entryData = button.entryData
|
|
if not entryData then
|
|
return
|
|
end
|
|
GameTooltip:SetOwner(button, "ANCHOR_RIGHT")
|
|
local alertText = RT:GetEntryAlertText(entryData)
|
|
local spellId = tonumber(entryData.spellId) or 0
|
|
if alertText then
|
|
GameTooltip:SetText(alertText)
|
|
if spellId > 0 then
|
|
GameTooltip:AddLine(GetSpellName(spellId) or ("Spell " .. tostring(spellId)), 1, 0.82, 0.15)
|
|
end
|
|
elseif spellId > 0 and type(GameTooltip.SetSpellByID) == "function" then
|
|
GameTooltip:SetSpellByID(spellId)
|
|
else
|
|
GameTooltip:SetText(RT:GetEntryDisplayText(entryData))
|
|
end
|
|
if RT:GetActionType(entryData) == "text" then
|
|
local targets = TrimText(entryData.targetSpec or "")
|
|
if targets ~= "" then
|
|
GameTooltip:AddLine(string.format("%s: %s", L["OPT_RT_ENTRY_TARGETS"] or "Targets", targets), 0.85, 0.85, 0.85)
|
|
end
|
|
elseif RT:GetTriggerType(entryData) == "bossAbility" then
|
|
local barName = TrimText(entryData.bossAbilityBarName or "")
|
|
if barName == "" then
|
|
local definition = RT:GetBossAbilityDefinition(RT:GetTimelineEditorState().encounterId, entryData.bossAbilityId)
|
|
barName = RT:GetBossAbilityDisplayText(definition)
|
|
end
|
|
GameTooltip:AddLine(string.format("%s: %s", L["OPT_RT_BOSS_BAR_NAME"] or "Bossmod bar name", barName ~= "" and barName or (L["OPT_RT_NO_BOSS_ABILITY"] or "No boss ability")), 0.85, 0.85, 0.85)
|
|
GameTooltip:AddLine(string.format("%s: %s", L["OPT_RT_CAST_COUNT"] or "Cast count", RT:FormatCastCountSelector(entryData.castCount)), 0.85, 0.85, 0.85)
|
|
elseif entryData.playerName and entryData.playerName ~= "" then
|
|
GameTooltip:AddLine(string.format("%s: %s", L["OPT_RT_ENTRY_PLAYER"] or "Fixed player", tostring(entryData.playerName)), 0.85, 0.85, 0.85)
|
|
end
|
|
HMGT:SafeShowTooltip(GameTooltip)
|
|
end)
|
|
marker:SetScript("OnLeave", function()
|
|
GameTooltip:Hide()
|
|
end)
|
|
marker:SetScript("OnDragStart", function(button)
|
|
if not RT:IsLocalEditor() then
|
|
return
|
|
end
|
|
button.dragging = true
|
|
end)
|
|
marker:SetScript("OnDragStop", function(button)
|
|
button.dragging = nil
|
|
end)
|
|
marker:SetScript("OnUpdate", function(button)
|
|
if not button.dragging then
|
|
return
|
|
end
|
|
local cursorX = GetCursorPosition()
|
|
local scale = UIParent:GetEffectiveScale() or 1
|
|
RT:HandleTimelineMarkerDrag(button.entryIndex, cursorX / scale)
|
|
end)
|
|
editor.markers[visibleCount] = marker
|
|
end
|
|
|
|
marker.entryIndex = index
|
|
marker.entryData = entry
|
|
local ratio = (timeSec - startSec) / duration
|
|
local x = math.floor(ratio * (width - 20)) + 10
|
|
marker:ClearAllPoints()
|
|
marker:SetPoint("BOTTOM", canvas, "BOTTOMLEFT", x, 84)
|
|
marker.stem:ClearAllPoints()
|
|
marker.stem:SetPoint("TOP", marker, "CENTER", 0, 0)
|
|
marker.stem:SetPoint("BOTTOM", canvas, "BOTTOMLEFT", x, 47)
|
|
marker.icon:SetTexture(self:GetEntryIcon(entry))
|
|
marker.label:SetText(FormatTimelineClock(timeSec))
|
|
if state.selectedIndex == index then
|
|
marker:SetBackdrop({
|
|
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
|
edgeSize = 12,
|
|
})
|
|
marker:SetBackdropBorderColor(0.25, 1, 0.6, 1)
|
|
else
|
|
marker:SetBackdrop(nil)
|
|
end
|
|
marker:Show()
|
|
end
|
|
end
|
|
for i = visibleCount + 1, #editor.markers do
|
|
editor.markers[i]:Hide()
|
|
end
|
|
|
|
if editor.emptyText then
|
|
editor.emptyText:SetShown(visibleCount == 0)
|
|
end
|
|
end
|
|
|
|
function RT:RefreshTimelineEditor()
|
|
if not self.timelineEditor then
|
|
return
|
|
end
|
|
self:RefreshTimelineEditorCanvas()
|
|
self:RefreshTimelineEditorDetails()
|
|
end
|
|
|
|
function RT:EnsureTimelineEditor()
|
|
if self.timelineEditor or not AceGUI then
|
|
return self.timelineEditor
|
|
end
|
|
|
|
local frame = AceGUI:Create("Frame")
|
|
frame:SetTitle(L["OPT_RT_TIMELINE_EDITOR"] or "Raid Timeline Editor")
|
|
frame:SetStatusText("")
|
|
frame:SetLayout("Fill")
|
|
frame:SetWidth(1080)
|
|
frame:SetHeight(580)
|
|
frame:EnableResize(true)
|
|
frame:SetCallback("OnClose", function(widget)
|
|
RT.timelineEditor = nil
|
|
if widget and widget.Release then
|
|
widget:Release()
|
|
end
|
|
end)
|
|
|
|
local root = AceGUI:Create("SimpleGroup")
|
|
root:SetFullWidth(true)
|
|
root:SetFullHeight(true)
|
|
root:SetLayout("Flow")
|
|
frame:AddChild(root)
|
|
|
|
local left = AceGUI:Create("SimpleGroup")
|
|
left:SetWidth(760)
|
|
left:SetFullHeight(true)
|
|
left:SetLayout("List")
|
|
root:AddChild(left)
|
|
|
|
local canvasGroup = AceGUI:Create("SimpleGroup")
|
|
canvasGroup:SetFullWidth(true)
|
|
canvasGroup:SetHeight(300)
|
|
canvasGroup:SetLayout("Fill")
|
|
left:AddChild(canvasGroup)
|
|
|
|
local viewportLabel = canvasGroup.frame:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
|
|
viewportLabel:SetPoint("TOPLEFT", canvasGroup.content, "TOPLEFT", 10, -8)
|
|
|
|
local canvasHolder = CreateFrame("Frame", nil, canvasGroup.content, "BackdropTemplate")
|
|
canvasHolder:SetPoint("TOPLEFT", canvasGroup.content, "TOPLEFT", 8, -30)
|
|
canvasHolder:SetPoint("TOPRIGHT", canvasGroup.content, "TOPRIGHT", -8, -30)
|
|
canvasHolder:SetHeight(250)
|
|
canvasHolder:SetBackdrop({
|
|
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
|
|
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
|
edgeSize = 12,
|
|
insets = { left = 3, right = 3, top = 3, bottom = 3 },
|
|
})
|
|
canvasHolder:SetBackdropBorderColor(1, 0.82, 0.15, 0.55)
|
|
canvasHolder:EnableMouse(true)
|
|
canvasHolder:EnableMouseWheel(true)
|
|
|
|
local axis = canvasHolder:CreateTexture(nil, "BACKGROUND")
|
|
axis:SetColorTexture(1, 0.82, 0.15, 0.9)
|
|
axis:SetPoint("BOTTOMLEFT", canvasHolder, "BOTTOMLEFT", 10, 44)
|
|
axis:SetPoint("BOTTOMRIGHT", canvasHolder, "BOTTOMRIGHT", -10, 44)
|
|
axis:SetHeight(3)
|
|
|
|
local emptyText = canvasHolder:CreateFontString(nil, "OVERLAY", "GameFontDisable")
|
|
emptyText:SetPoint("CENTER", canvasHolder, "CENTER", 0, 20)
|
|
emptyText:SetText(L["OPT_RT_TIMELINE_EMPTY_WINDOW"] or "No cooldowns in the current window.")
|
|
|
|
local tickLines, tickLabels = {}, {}
|
|
for i = 1, 12 do
|
|
local line = canvasHolder:CreateTexture(nil, "BORDER")
|
|
line:SetColorTexture(1, 1, 1, 0.2)
|
|
line:Hide()
|
|
tickLines[i] = line
|
|
local label = canvasHolder:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
|
label:SetTextColor(1, 0.82, 0.15)
|
|
label:Hide()
|
|
tickLabels[i] = label
|
|
end
|
|
|
|
canvasHolder:SetScript("OnMouseDown", function(_, button)
|
|
if button ~= "LeftButton" then
|
|
return
|
|
end
|
|
local cursorX = GetCursorPosition()
|
|
local scale = UIParent:GetEffectiveScale() or 1
|
|
RT:CreateTimelineEntryAtCursor(cursorX / scale)
|
|
end)
|
|
canvasHolder:SetScript("OnMouseWheel", function(_, delta)
|
|
local state = RT:GetTimelineEditorState()
|
|
if IsControlKeyDown() then
|
|
state.viewportDuration = Clamp(state.viewportDuration - (delta * 15), 30, 300, state.viewportDuration)
|
|
else
|
|
state.viewportStart = Clamp(state.viewportStart - (delta * 15), 0, math.max(0, 600 - state.viewportDuration), state.viewportStart)
|
|
end
|
|
RT:RefreshTimelineEditor()
|
|
end)
|
|
|
|
local viewportSlider = AceGUI:Create("Slider")
|
|
viewportSlider:SetLabel(L["OPT_RT_TIMELINE_SCROLL"] or "Scroll timeline")
|
|
viewportSlider:SetSliderValues(0, 480, 1)
|
|
viewportSlider:SetValue(0)
|
|
viewportSlider:SetWidth(360)
|
|
viewportSlider:SetCallback("OnValueChanged", function(_, _, value)
|
|
RT:GetTimelineEditorState().viewportStart = math.floor((tonumber(value) or 0) + 0.5)
|
|
RT:RefreshTimelineEditor()
|
|
end)
|
|
left:AddChild(viewportSlider)
|
|
|
|
local zoomSlider = AceGUI:Create("Slider")
|
|
zoomSlider:SetLabel(L["OPT_RT_TIMELINE_ZOOM"] or "Zoom")
|
|
zoomSlider:SetSliderValues(30, 300, 15)
|
|
zoomSlider:SetValue(120)
|
|
zoomSlider:SetWidth(300)
|
|
zoomSlider:SetCallback("OnValueChanged", function(_, _, value)
|
|
RT:GetTimelineEditorState().viewportDuration = math.floor((tonumber(value) or 120) + 0.5)
|
|
RT:RefreshTimelineEditor()
|
|
end)
|
|
left:AddChild(zoomSlider)
|
|
|
|
local hint = AceGUI:Create("Label")
|
|
hint:SetFullWidth(true)
|
|
hint:SetText(L["OPT_RT_TIMELINE_HINT"] or "Click the bar to add a cooldown. Drag markers horizontally to change the time. Mousewheel scrolls, Ctrl+Mousewheel zooms.")
|
|
left:AddChild(hint)
|
|
|
|
local right = AceGUI:Create("InlineGroup")
|
|
right:SetTitle(L["OPT_RT_ASSIGNMENT_EDITOR"] or "Assignment")
|
|
right:SetWidth(280)
|
|
right:SetFullHeight(true)
|
|
right:SetLayout("Fill")
|
|
root:AddChild(right)
|
|
|
|
local rightScroll = AceGUI:Create("ScrollFrame")
|
|
rightScroll:SetLayout("List")
|
|
right:AddChild(rightScroll)
|
|
|
|
local selectionHeader = AceGUI:Create("Label")
|
|
selectionHeader:SetFullWidth(true)
|
|
selectionHeader:SetHeight(42)
|
|
selectionHeader:SetText(L["OPT_RT_ASSIGNMENT_NONE"] or "No cooldown selected")
|
|
if selectionHeader.label then
|
|
selectionHeader.label:SetJustifyH("LEFT")
|
|
selectionHeader.label:SetJustifyV("TOP")
|
|
end
|
|
rightScroll:AddChild(selectionHeader)
|
|
|
|
local timeValue = AceGUI:Create("EditBox")
|
|
timeValue:SetLabel(L["OPT_RT_ENTRY_TIME"] or "Time")
|
|
timeValue:SetFullWidth(true)
|
|
timeValue:SetCallback("OnEnterPressed", function(widget, _, value)
|
|
local state = RT:GetTimelineEditorState()
|
|
if state.encounterId and state.selectedIndex then
|
|
RT:SetEntryField(state.encounterId, state.selectedIndex, "time", value)
|
|
RT:RefreshTimelineEditor()
|
|
end
|
|
widget:ClearFocus()
|
|
end)
|
|
rightScroll:AddChild(timeValue)
|
|
|
|
local triggerTypeValue = AceGUI:Create("Dropdown")
|
|
triggerTypeValue:SetLabel(L["OPT_RT_TRIGGER"] or "Trigger")
|
|
triggerTypeValue:SetFullWidth(true)
|
|
triggerTypeValue:SetList(RT:GetTriggerTypeValues())
|
|
triggerTypeValue:SetCallback("OnValueChanged", function(_, _, value)
|
|
local state = RT:GetTimelineEditorState()
|
|
if state.encounterId and state.selectedIndex then
|
|
RT:SetEntryField(state.encounterId, state.selectedIndex, "triggerType", value)
|
|
RT:RefreshTimelineEditor()
|
|
end
|
|
end)
|
|
rightScroll:AddChild(triggerTypeValue)
|
|
|
|
local actionTypeValue = AceGUI:Create("Dropdown")
|
|
actionTypeValue:SetLabel(L["OPT_RT_ACTION"] or "Action")
|
|
actionTypeValue:SetFullWidth(true)
|
|
actionTypeValue:SetList(RT:GetActionTypeValues())
|
|
actionTypeValue:SetCallback("OnValueChanged", function(_, _, value)
|
|
local state = RT:GetTimelineEditorState()
|
|
if state.encounterId and state.selectedIndex then
|
|
RT:SetEntryField(state.encounterId, state.selectedIndex, "actionType", value)
|
|
RT:RefreshTimelineEditor()
|
|
end
|
|
end)
|
|
rightScroll:AddChild(actionTypeValue)
|
|
|
|
local spellValue = AceGUI:Create("Dropdown")
|
|
spellValue:SetLabel(L["OPT_RT_ENTRY_SPELL"] or "Spell")
|
|
spellValue:SetFullWidth(true)
|
|
spellValue:SetList(RT:GetRaidCooldownSpellValues())
|
|
spellValue:SetCallback("OnValueChanged", function(_, _, value)
|
|
local state = RT:GetTimelineEditorState()
|
|
if state.encounterId and state.selectedIndex then
|
|
RT:SetEntryField(state.encounterId, state.selectedIndex, "spellId", value)
|
|
RT:RefreshTimelineEditor()
|
|
end
|
|
end)
|
|
rightScroll:AddChild(spellValue)
|
|
|
|
local castCountValue = AceGUI:Create("EditBox")
|
|
castCountValue:SetLabel(string.format(
|
|
"%s (1 / %s / %s / %s)",
|
|
L["OPT_RT_CAST_COUNT"] or "Cast count",
|
|
L["OPT_RT_CAST_ALL"] or "All",
|
|
L["OPT_RT_CAST_ODD"] or "Odd",
|
|
L["OPT_RT_CAST_EVEN"] or "Even"
|
|
))
|
|
castCountValue:SetFullWidth(true)
|
|
castCountValue:SetCallback("OnEnterPressed", function(widget, _, value)
|
|
local state = RT:GetTimelineEditorState()
|
|
if state.encounterId and state.selectedIndex then
|
|
RT:SetEntryField(state.encounterId, state.selectedIndex, "castCount", value)
|
|
RT:RefreshTimelineEditor()
|
|
end
|
|
widget:ClearFocus()
|
|
end)
|
|
rightScroll:AddChild(castCountValue)
|
|
|
|
local bossAbilityBarNameValue = AceGUI:Create("EditBox")
|
|
bossAbilityBarNameValue:SetLabel(L["OPT_RT_BOSS_BAR_NAME"] or "Bossmod bar name")
|
|
bossAbilityBarNameValue:SetFullWidth(true)
|
|
bossAbilityBarNameValue:SetCallback("OnEnterPressed", function(widget, _, value)
|
|
local state = RT:GetTimelineEditorState()
|
|
if state.encounterId and state.selectedIndex then
|
|
RT:SetEntryField(state.encounterId, state.selectedIndex, "bossAbilityBarName", value)
|
|
RT:RefreshTimelineEditor()
|
|
end
|
|
widget:ClearFocus()
|
|
end)
|
|
rightScroll:AddChild(bossAbilityBarNameValue)
|
|
|
|
local textValue = AceGUI:Create("EditBox")
|
|
textValue:SetLabel(L["OPT_RT_ENTRY_TEXT"] or "Custom text")
|
|
textValue:SetFullWidth(true)
|
|
textValue:SetCallback("OnEnterPressed", function(widget, _, value)
|
|
local state = RT:GetTimelineEditorState()
|
|
if state.encounterId and state.selectedIndex then
|
|
RT:SetEntryField(state.encounterId, state.selectedIndex, "alertText", value)
|
|
RT:RefreshTimelineEditor()
|
|
end
|
|
widget:ClearFocus()
|
|
end)
|
|
rightScroll:AddChild(textValue)
|
|
|
|
local targetValue = AceGUI:Create("EditBox")
|
|
targetValue:SetLabel(L["OPT_RT_ENTRY_PLAYER"] or "Fixed player")
|
|
targetValue:SetFullWidth(true)
|
|
targetValue:SetCallback("OnEnterPressed", function(widget, _, value)
|
|
local state = RT:GetTimelineEditorState()
|
|
if state.encounterId and state.selectedIndex then
|
|
local encounter = RT:GetEncounter(state.encounterId)
|
|
local entry = encounter and encounter.entries and encounter.entries[state.selectedIndex] or nil
|
|
local field = (entry and RT:GetActionType(entry) == "text") and "targetSpec" or "playerName"
|
|
RT:SetEntryField(state.encounterId, state.selectedIndex, field, value)
|
|
RT:RefreshTimelineEditor()
|
|
end
|
|
widget:ClearFocus()
|
|
end)
|
|
rightScroll:AddChild(targetValue)
|
|
|
|
local deleteButton = AceGUI:Create("Button")
|
|
deleteButton:SetText(REMOVE or "Remove")
|
|
deleteButton:SetFullWidth(true)
|
|
deleteButton:SetCallback("OnClick", function()
|
|
RT:DeleteSelectedTimelineEntry()
|
|
end)
|
|
rightScroll:AddChild(deleteButton)
|
|
|
|
self.timelineEditor = {
|
|
frame = frame,
|
|
canvas = canvasHolder,
|
|
viewportLabel = viewportLabel,
|
|
viewportSlider = viewportSlider,
|
|
zoomSlider = zoomSlider,
|
|
selectionHeader = selectionHeader,
|
|
timeValue = timeValue,
|
|
triggerTypeValue = triggerTypeValue,
|
|
actionTypeValue = actionTypeValue,
|
|
spellValue = spellValue,
|
|
castCountValue = castCountValue,
|
|
bossAbilityBarNameValue = bossAbilityBarNameValue,
|
|
textValue = textValue,
|
|
targetValue = targetValue,
|
|
deleteButton = deleteButton,
|
|
tickLines = tickLines,
|
|
tickLabels = tickLabels,
|
|
emptyText = emptyText,
|
|
markers = {},
|
|
}
|
|
return self.timelineEditor
|
|
end
|
|
|
|
function RT:OpenTimelineEditor(encounterId)
|
|
if not AceGUI then
|
|
return
|
|
end
|
|
local encounter = self:GetEncounter(encounterId)
|
|
if not encounter then
|
|
return
|
|
end
|
|
local state = self:GetTimelineEditorState()
|
|
state.encounterId = tonumber(encounterId)
|
|
state.selectedIndex = nil
|
|
state.viewportStart = Clamp(state.viewportStart, 0, 540, 0)
|
|
local editor = self:EnsureTimelineEditor()
|
|
if not editor then
|
|
return
|
|
end
|
|
editor.frame:SetTitle(string.format("%s: %s (%d)", L["OPT_RT_TIMELINE_EDITOR"] or "Raid Timeline Editor", encounter.name ~= "" and encounter.name or (L["OPT_RT_ENCOUNTER"] or "Encounter"), tonumber(encounterId) or 0))
|
|
self:RefreshTimelineEditor()
|
|
end
|
|
|
|
function RT:GetAlertFontPath()
|
|
local settings = self:GetSettings()
|
|
if LSM and settings and type(LSM.Fetch) == "function" then
|
|
local fontPath = LSM:Fetch("font", settings.alertFont, true)
|
|
if fontPath and fontPath ~= "" then
|
|
return fontPath
|
|
end
|
|
end
|
|
return STANDARD_TEXT_FONT
|
|
end
|
|
|
|
function RT:SaveAlertPosition()
|
|
local frame = self.alertFrame
|
|
local settings = self:GetSettings()
|
|
if not frame or not settings then
|
|
return
|
|
end
|
|
local frameCenterX, frameCenterY = frame:GetCenter()
|
|
local parentCenterX, parentCenterY = UIParent:GetCenter()
|
|
if not frameCenterX or not frameCenterY or not parentCenterX or not parentCenterY then
|
|
return
|
|
end
|
|
settings.alertPosX = math.floor(frameCenterX - parentCenterX + 0.5)
|
|
settings.alertPosY = math.floor(frameCenterY - parentCenterY + 0.5)
|
|
end
|
|
|
|
function RT:ApplyAlertStyle()
|
|
local frame = self.alertFrame
|
|
local settings = self:GetSettings()
|
|
if not frame or not settings then
|
|
return
|
|
end
|
|
|
|
frame:ClearAllPoints()
|
|
frame:SetPoint("CENTER", UIParent, "CENTER", settings.alertPosX or 0, settings.alertPosY or 180)
|
|
frame:EnableMouse(settings.unlocked == true)
|
|
if frame.handle then
|
|
frame.handle:SetShown(settings.unlocked == true)
|
|
end
|
|
|
|
if frame.text then
|
|
frame.text:SetFont(self:GetAlertFontPath(), settings.alertFontSize or 30, settings.alertFontOutline or "OUTLINE")
|
|
frame.text:SetTextColor(settings.alertColor.r, settings.alertColor.g, settings.alertColor.b, settings.alertColor.a)
|
|
end
|
|
end
|
|
|
|
function RT:EnsureAlertFrame()
|
|
if self.alertFrame then
|
|
return self.alertFrame
|
|
end
|
|
|
|
local frame = CreateFrame("Frame", "HMGT_RaidTimelineAlertFrame", UIParent, "BackdropTemplate")
|
|
frame:SetSize(640, 120)
|
|
frame:SetFrameStrata("FULLSCREEN_DIALOG")
|
|
frame:SetFrameLevel(200)
|
|
frame:SetClampedToScreen(true)
|
|
frame:SetMovable(true)
|
|
frame:RegisterForDrag("LeftButton")
|
|
frame:SetScript("OnDragStart", function(selfFrame)
|
|
if RT:GetSettings().unlocked == true then
|
|
selfFrame:StartMoving()
|
|
end
|
|
end)
|
|
frame:SetScript("OnDragStop", function(selfFrame)
|
|
selfFrame:StopMovingOrSizing()
|
|
RT:SaveAlertPosition()
|
|
RT:ApplyAlertStyle()
|
|
end)
|
|
frame:Hide()
|
|
|
|
local handle = frame:CreateTexture(nil, "BACKGROUND")
|
|
handle:SetAllPoints(frame)
|
|
handle:SetColorTexture(0, 0, 0, 0.35)
|
|
frame.handle = handle
|
|
|
|
local text = frame:CreateFontString(nil, "OVERLAY")
|
|
text:SetPoint("CENTER", frame, "CENTER", 0, 0)
|
|
text:SetPoint("LEFT", frame, "LEFT", 18, 0)
|
|
text:SetPoint("RIGHT", frame, "RIGHT", -18, 0)
|
|
text:SetJustifyH("CENTER")
|
|
text:SetJustifyV("MIDDLE")
|
|
frame.text = text
|
|
|
|
self.alertFrame = frame
|
|
self:ApplyAlertStyle()
|
|
if frame.text then
|
|
frame.text:SetText(L["OPT_RT_ALERT_PREVIEW"] or "Tranquility in 5")
|
|
end
|
|
return frame
|
|
end
|
|
|
|
function RT:ShowPreview()
|
|
local frame = self:EnsureAlertFrame()
|
|
if not frame then
|
|
return
|
|
end
|
|
self.previewAlertActive = true
|
|
frame:Show()
|
|
if frame.text then
|
|
frame.text:SetText(L["OPT_RT_ALERT_PREVIEW"] or "Tranquility in 5")
|
|
end
|
|
end
|
|
|
|
function RT:HideAlert()
|
|
self.previewAlertActive = false
|
|
self.currentAlert = nil
|
|
if not self.alertFrame then
|
|
return
|
|
end
|
|
if self:GetSettings().unlocked == true then
|
|
self:ApplyAlertStyle()
|
|
self.alertFrame:Show()
|
|
if self.alertFrame.text then
|
|
self.alertFrame.text:SetText(L["OPT_RT_ALERT_UNLOCKED_HINT"] or "Raid Timeline Alert\nDrag to move")
|
|
end
|
|
else
|
|
self.alertFrame:Hide()
|
|
end
|
|
end
|
|
|
|
function RT:ShowCountdownAlert(spellId, secondsRemaining, alertText)
|
|
local frame = self:EnsureAlertFrame()
|
|
if not frame then
|
|
return
|
|
end
|
|
self.currentAlert = {
|
|
spellId = math.max(0, tonumber(spellId) or 0),
|
|
alertText = tostring(alertText or ""),
|
|
endTime = GetTime() + math.max(0, tonumber(secondsRemaining) or 0),
|
|
hideTime = GetTime() + math.max(0, tonumber(secondsRemaining) or 0) + 1,
|
|
}
|
|
self:UpdateCurrentAlert(true)
|
|
end
|
|
|
|
local function GetAlertSpellLabel(spellId)
|
|
local sid = tonumber(spellId) or 0
|
|
if sid <= 0 then
|
|
return nil
|
|
end
|
|
local icon = RT:GetEntryIcon({ spellId = sid, actionType = "raidCooldown", triggerType = "time" })
|
|
local spellName = GetSpellName(sid) or ("Spell " .. tostring(sid))
|
|
if icon and icon ~= "" then
|
|
return string.format("|T%s:22:22:0:0:64:64:5:59:5:59|t %s", tostring(icon), spellName)
|
|
end
|
|
return spellName
|
|
end
|
|
|
|
local function GetAlertTextLabel(alertText)
|
|
local text = TrimText(alertText)
|
|
if text == "" then
|
|
return nil
|
|
end
|
|
return text
|
|
end
|
|
|
|
function RT:UpdateCurrentAlert(forceShow)
|
|
local alert = self.currentAlert
|
|
if not alert then
|
|
return
|
|
end
|
|
|
|
local frame = self:EnsureAlertFrame()
|
|
local now = GetTime()
|
|
local remaining = (tonumber(alert.endTime) or 0) - now
|
|
local displayValue = math.ceil(math.max(0, remaining))
|
|
local label = nil
|
|
if (tonumber(alert.spellId) or 0) > 0 then
|
|
label = GetAlertSpellLabel(alert.spellId)
|
|
end
|
|
if not label or label == "" then
|
|
label = GetAlertTextLabel(alert.alertText) or self:GetEntryDisplayText({ spellId = alert.spellId, alertText = alert.alertText, actionType = "text", triggerType = "time" })
|
|
end
|
|
|
|
if frame and frame.text then
|
|
if remaining <= 0.05 then
|
|
frame.text:SetText(label)
|
|
else
|
|
frame.text:SetText(string.format(L["OPT_RT_ALERT_TEMPLATE"] or "%s in %ds", label, displayValue))
|
|
end
|
|
end
|
|
if frame and (forceShow or not frame:IsShown()) then
|
|
frame:Show()
|
|
end
|
|
if now >= (tonumber(alert.hideTime) or 0) then
|
|
self:HideAlert()
|
|
end
|
|
end
|
|
|
|
function RT:GetRosterEntries()
|
|
local entries = {}
|
|
local seen = {}
|
|
|
|
local function addUnit(unitId, rosterIndex, subgroup)
|
|
if not unitId or not UnitExists(unitId) or not UnitIsPlayer(unitId) then
|
|
return
|
|
end
|
|
local name = NormalizeName(UnitName(unitId))
|
|
if not name or seen[name] then
|
|
return
|
|
end
|
|
seen[name] = true
|
|
entries[#entries + 1] = {
|
|
name = name,
|
|
index = tonumber(rosterIndex) or (#entries + 1),
|
|
subgroup = tonumber(subgroup) or 1,
|
|
unitId = unitId,
|
|
}
|
|
end
|
|
|
|
if IsInRaid() then
|
|
for i = 1, GetNumGroupMembers() do
|
|
local _, _, subgroup = GetRaidRosterInfo(i)
|
|
addUnit("raid" .. i, i, subgroup)
|
|
end
|
|
else
|
|
addUnit("player", 1, 1)
|
|
if IsInGroup() then
|
|
for i = 1, GetNumSubgroupMembers() do
|
|
addUnit("party" .. i, i + 1, 1)
|
|
end
|
|
end
|
|
end
|
|
|
|
table.sort(entries, function(a, b)
|
|
return (tonumber(a.index) or 0) < (tonumber(b.index) or 0)
|
|
end)
|
|
return entries
|
|
end
|
|
|
|
function RT:GetRosterPlayers()
|
|
local players = {}
|
|
for _, entry in ipairs(self:GetRosterEntries()) do
|
|
players[#players + 1] = entry.name
|
|
end
|
|
return players
|
|
end
|
|
|
|
function RT:GetResolvedTargetPlayers(targetSpec)
|
|
local spec = tostring(targetSpec or "")
|
|
spec = spec:gsub("^%s+", ""):gsub("%s+$", "")
|
|
if spec == "" then
|
|
return {}
|
|
end
|
|
|
|
local roster = self:GetRosterEntries()
|
|
local selected = {}
|
|
local seen = {}
|
|
|
|
local function addName(name)
|
|
local normalized = NormalizeName(name)
|
|
if normalized and normalized ~= "" and not seen[normalized] and HMGT:IsPlayerInCurrentGroup(normalized) then
|
|
seen[normalized] = true
|
|
selected[#selected + 1] = normalized
|
|
end
|
|
end
|
|
|
|
local function addEntry(entry)
|
|
if entry and entry.name then
|
|
addName(entry.name)
|
|
end
|
|
end
|
|
|
|
local function addNumericTarget(startValue, endValue)
|
|
local fromValue = math.min(tonumber(startValue) or 0, tonumber(endValue) or 0)
|
|
local toValue = math.max(tonumber(startValue) or 0, tonumber(endValue) or 0)
|
|
for _, entry in ipairs(roster) do
|
|
local rosterValue = IsInRaid() and tonumber(entry.subgroup) or tonumber(entry.index)
|
|
if rosterValue and rosterValue >= fromValue and rosterValue <= toValue then
|
|
addEntry(entry)
|
|
end
|
|
end
|
|
end
|
|
|
|
for token in string.gmatch(spec, "([^,]+)") do
|
|
local trimmed = tostring(token or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
|
local lower = trimmed:lower()
|
|
if trimmed ~= "" then
|
|
if lower == "all" then
|
|
for _, entry in ipairs(roster) do addEntry(entry) end
|
|
elseif lower == "groupodd" then
|
|
for _, entry in ipairs(roster) do
|
|
local groupValue = IsInRaid() and tonumber(entry.subgroup) or tonumber(entry.index)
|
|
if ((groupValue or 0) % 2) == 1 then addEntry(entry) end
|
|
end
|
|
elseif lower == "groupeven" then
|
|
for _, entry in ipairs(roster) do
|
|
local groupValue = IsInRaid() and tonumber(entry.subgroup) or tonumber(entry.index)
|
|
if ((groupValue or 0) % 2) == 0 and (groupValue or 0) > 0 then addEntry(entry) end
|
|
end
|
|
elseif lower == "odd" then
|
|
for _, entry in ipairs(roster) do
|
|
if ((tonumber(entry.index) or 0) % 2) == 1 then addEntry(entry) end
|
|
end
|
|
elseif lower == "even" then
|
|
for _, entry in ipairs(roster) do
|
|
if ((tonumber(entry.index) or 0) % 2) == 0 then addEntry(entry) end
|
|
end
|
|
else
|
|
local groupNumber = trimmed:match("^[Gg]roup(%d+)$")
|
|
if groupNumber then
|
|
addNumericTarget(groupNumber, groupNumber)
|
|
else
|
|
local rangeStart, rangeEnd = trimmed:match("^(%d+)%s*%-%s*(%d+)$")
|
|
if rangeStart and rangeEnd then
|
|
addNumericTarget(rangeStart, rangeEnd)
|
|
else
|
|
local num = tonumber(trimmed)
|
|
if num then
|
|
addNumericTarget(num, num)
|
|
else
|
|
addName(trimmed)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
table.sort(selected, function(a, b)
|
|
local aIndex, bIndex = 999, 999
|
|
for _, entry in ipairs(roster) do
|
|
if entry.name == a then aIndex = tonumber(entry.index) or 999 end
|
|
if entry.name == b then bIndex = tonumber(entry.index) or 999 end
|
|
end
|
|
if aIndex ~= bIndex then return aIndex < bIndex end
|
|
return tostring(a) < tostring(b)
|
|
end)
|
|
return selected
|
|
end
|
|
|
|
function RT:GetTextTargetPlayers(targetSpec)
|
|
return self:GetResolvedTargetPlayers(targetSpec)
|
|
end
|
|
|
|
function RT:IsSpellReadyForPlayer(playerName, spellId)
|
|
local name = NormalizeName(playerName)
|
|
local sid = tonumber(spellId)
|
|
if not name or not sid or sid <= 0 then
|
|
return false
|
|
end
|
|
if not HMGT:IsPlayerInCurrentGroup(name) then
|
|
return false
|
|
end
|
|
if not HMGT:IsTrackedSpellKnownForPlayer(name, sid) then
|
|
return false
|
|
end
|
|
|
|
local spellEntry = self:GetSpellEntry(sid)
|
|
local remaining, _, currentCharges, maxCharges = HMGT:GetCooldownInfo(name, sid)
|
|
if spellEntry and HMGT:IsAvailabilitySpell(spellEntry) then
|
|
return (tonumber(maxCharges) or 0) > 0 and (tonumber(currentCharges) or 0) >= (tonumber(maxCharges) or 0)
|
|
end
|
|
if (tonumber(maxCharges) or 0) > 0 then
|
|
return (tonumber(currentCharges) or 0) > 0
|
|
end
|
|
return (tonumber(remaining) or 0) <= 0
|
|
end
|
|
|
|
function RT:GetReadyCandidates(spellId)
|
|
local candidates = {}
|
|
for _, playerName in ipairs(self:GetRosterPlayers()) do
|
|
if self:IsSpellReadyForPlayer(playerName, spellId) then
|
|
candidates[#candidates + 1] = playerName
|
|
end
|
|
end
|
|
table.sort(candidates)
|
|
return candidates
|
|
end
|
|
|
|
function RT:IsLocalEditor()
|
|
return HMGT.IsRaidTimelineEditor and HMGT:IsRaidTimelineEditor() or true
|
|
end
|
|
|
|
function RT:IsAuthorizedSender(playerName)
|
|
return HMGT.IsRaidTimelineEditor and HMGT:IsRaidTimelineEditor(playerName) or false
|
|
end
|
|
|
|
function RT:GetBossAbilityDispatchKey(entryIndex, castCount)
|
|
return string.format("%s:%s", tostring(entryIndex or 0), tostring(math.max(1, tonumber(castCount) or 1)))
|
|
end
|
|
|
|
function RT:LogBossAbilityProbe(entryIndex, entry, stateKey, fmt, ...)
|
|
self._bossAbilityProbeStates = self._bossAbilityProbeStates or {}
|
|
local encounterId = tonumber(self.activeEncounterId) or 0
|
|
local slot = tonumber(entryIndex) or 0
|
|
local cacheKey = string.format("%d:%d", encounterId, slot)
|
|
if self._bossAbilityProbeStates[cacheKey] == stateKey then
|
|
return
|
|
end
|
|
self._bossAbilityProbeStates[cacheKey] = stateKey
|
|
HMGT:DebugScoped("verbose", "RaidTimeline", fmt, ...)
|
|
end
|
|
|
|
function RT:ProbeBossAbilityEntry(entryIndex, entry)
|
|
-- Bossmod-specific matching lives in separate integration files.
|
|
end
|
|
|
|
function RT:GetNextBossAbilityCount(bossAbilityId, explicitCount)
|
|
local numericCount = tonumber(explicitCount)
|
|
if numericCount and numericCount > 0 then
|
|
return math.floor(numericCount + 0.5)
|
|
end
|
|
self.activeBossAbilityCounts = self.activeBossAbilityCounts or {}
|
|
local key = tostring(bossAbilityId or "")
|
|
self.activeBossAbilityCounts[key] = math.max(0, tonumber(self.activeBossAbilityCounts[key]) or 0) + 1
|
|
return self.activeBossAbilityCounts[key]
|
|
end
|
|
|
|
function RT:GetWhisperTargetForPlayer(playerName)
|
|
local unitId = HMGT.GetUnitForPlayer and HMGT:GetUnitForPlayer(playerName) or nil
|
|
if unitId and UnitExists(unitId) then
|
|
local fullName = GetUnitName(unitId, true)
|
|
if fullName and fullName ~= "" then
|
|
return fullName
|
|
end
|
|
end
|
|
return playerName
|
|
end
|
|
|
|
function RT:SelectAssignedPlayer(encounterId, entry, entryIndex)
|
|
local spellId = tonumber(entry and entry.spellId) or 0
|
|
local desired = NormalizeName(entry and entry.playerName)
|
|
local targetSpec = TrimText(entry and (entry.targetSpec or entry.playerName) or "")
|
|
if spellId <= 0 then
|
|
return nil
|
|
end
|
|
|
|
local candidates = self:GetReadyCandidates(spellId)
|
|
local targetPlayers = self:GetResolvedTargetPlayers(targetSpec)
|
|
local isSingleExactTarget = desired and desired ~= "" and targetSpec ~= "" and not targetSpec:find(",") and #targetPlayers == 1 and targetPlayers[1] == desired
|
|
if #candidates == 0 then
|
|
HMGT:DebugScoped(
|
|
"verbose",
|
|
"RaidTimeline",
|
|
"RaidTimeline select encounter=%s slot=%s spellId=%s desired=%s target=%s candidates=0 selected=nil",
|
|
tostring(encounterId),
|
|
tostring(entryIndex),
|
|
tostring(spellId),
|
|
tostring(desired or ""),
|
|
tostring(targetSpec)
|
|
)
|
|
return nil
|
|
end
|
|
if isSingleExactTarget then
|
|
for i = 1, #candidates do
|
|
if candidates[i] == desired then
|
|
HMGT:DebugScoped(
|
|
"verbose",
|
|
"RaidTimeline",
|
|
"RaidTimeline select encounter=%s slot=%s spellId=%s desired=%s target=%s candidates=%s selected=%s mode=target-exact",
|
|
tostring(encounterId),
|
|
tostring(entryIndex),
|
|
tostring(spellId),
|
|
tostring(desired),
|
|
tostring(targetSpec),
|
|
table.concat(candidates, ", "),
|
|
tostring(candidates[i])
|
|
)
|
|
return candidates[i]
|
|
end
|
|
end
|
|
for i = 1, #candidates do
|
|
if candidates[i] > desired then
|
|
HMGT:DebugScoped(
|
|
"verbose",
|
|
"RaidTimeline",
|
|
"RaidTimeline select encounter=%s slot=%s spellId=%s desired=%s target=%s candidates=%s selected=%s mode=target-fallback-next",
|
|
tostring(encounterId),
|
|
tostring(entryIndex),
|
|
tostring(spellId),
|
|
tostring(desired),
|
|
tostring(targetSpec),
|
|
table.concat(candidates, ", "),
|
|
tostring(candidates[i])
|
|
)
|
|
return candidates[i]
|
|
end
|
|
end
|
|
HMGT:DebugScoped(
|
|
"verbose",
|
|
"RaidTimeline",
|
|
"RaidTimeline select encounter=%s slot=%s spellId=%s desired=%s target=%s candidates=%s selected=%s mode=target-fallback-wrap",
|
|
tostring(encounterId),
|
|
tostring(entryIndex),
|
|
tostring(spellId),
|
|
tostring(desired),
|
|
tostring(targetSpec),
|
|
table.concat(candidates, ", "),
|
|
tostring(candidates[1])
|
|
)
|
|
return candidates[1]
|
|
end
|
|
if #targetPlayers > 0 then
|
|
local allowed = {}
|
|
local filtered = {}
|
|
for _, playerName in ipairs(targetPlayers) do
|
|
allowed[playerName] = true
|
|
end
|
|
for _, playerName in ipairs(candidates) do
|
|
if allowed[playerName] then
|
|
filtered[#filtered + 1] = playerName
|
|
end
|
|
end
|
|
if #filtered > 0 then
|
|
local hash = StableHash(encounterId, spellId, targetSpec, tonumber(entry.time) or 0, tonumber(entryIndex) or 0)
|
|
local selected = filtered[(hash % #filtered) + 1]
|
|
HMGT:DebugScoped(
|
|
"verbose",
|
|
"RaidTimeline",
|
|
"RaidTimeline select encounter=%s slot=%s spellId=%s target=%s candidates=%s filtered=%s hash=%s selected=%s mode=target-filter",
|
|
tostring(encounterId),
|
|
tostring(entryIndex),
|
|
tostring(spellId),
|
|
tostring(targetSpec),
|
|
table.concat(candidates, ", "),
|
|
table.concat(filtered, ", "),
|
|
tostring(hash),
|
|
tostring(selected)
|
|
)
|
|
return selected
|
|
end
|
|
HMGT:DebugScoped(
|
|
"verbose",
|
|
"RaidTimeline",
|
|
"RaidTimeline select encounter=%s slot=%s spellId=%s target=%s candidates=%s filtered=0 selected=nil mode=target-filter-empty",
|
|
tostring(encounterId),
|
|
tostring(entryIndex),
|
|
tostring(spellId),
|
|
tostring(targetSpec),
|
|
table.concat(candidates, ", ")
|
|
)
|
|
return nil
|
|
end
|
|
local hash = StableHash(encounterId, spellId, tonumber(entry.time) or 0, tonumber(entryIndex) or 0)
|
|
local selected = candidates[(hash % #candidates) + 1]
|
|
HMGT:DebugScoped(
|
|
"verbose",
|
|
"RaidTimeline",
|
|
"RaidTimeline select encounter=%s slot=%s spellId=%s target=%s candidates=%s hash=%s selected=%s mode=hash",
|
|
tostring(encounterId),
|
|
tostring(entryIndex),
|
|
tostring(spellId),
|
|
tostring(targetSpec),
|
|
table.concat(candidates, ", "),
|
|
tostring(hash),
|
|
tostring(selected)
|
|
)
|
|
return selected
|
|
end
|
|
|
|
function RT:GetAssignedPlayersForEntry(encounterId, entry, entryIndex)
|
|
local actionType = self:GetActionType(entry)
|
|
if actionType == "text" then
|
|
local targetSpec = TrimText(entry and (entry.targetSpec or entry.playerName) or "")
|
|
if targetSpec == "" then
|
|
return self:GetRosterPlayers()
|
|
end
|
|
return self:GetResolvedTargetPlayers(targetSpec)
|
|
elseif self:GetTriggerType(entry) == "bossAbility" and actionType == "raidCooldown" then
|
|
local selected = self:SelectAssignedPlayer(encounterId, entry, entryIndex)
|
|
return selected and { selected } or {}
|
|
elseif self:GetTriggerType(entry) == "bossAbility" then
|
|
local targetSpec = TrimText(entry and (entry.targetSpec or entry.playerName) or "")
|
|
if targetSpec == "" then
|
|
return self:GetRosterPlayers()
|
|
end
|
|
return self:GetResolvedTargetPlayers(targetSpec)
|
|
end
|
|
local selected = self:SelectAssignedPlayer(encounterId, entry, entryIndex)
|
|
return selected and { selected } or {}
|
|
end
|
|
|
|
function RT:StoreAssignment(encounterId, timeSec, spellId, leadTime, alertText)
|
|
local encounterKey = tonumber(encounterId)
|
|
local timeValue = tonumber(timeSec)
|
|
local spellValue = math.max(0, tonumber(spellId) or 0)
|
|
local leadValue = tonumber(leadTime)
|
|
local textValue = tostring(alertText or "")
|
|
if not encounterKey or not timeValue or not leadValue then
|
|
return
|
|
end
|
|
self.receivedAssignments = self.receivedAssignments or {}
|
|
self.receivedAssignments[encounterKey] = self.receivedAssignments[encounterKey] or {}
|
|
local assignments = self.receivedAssignments[encounterKey]
|
|
local key = string.format("%d:%d:%s", timeValue, spellValue, self:EncodeCommText(textValue))
|
|
assignments[key] = {
|
|
time = timeValue,
|
|
spellId = spellValue,
|
|
leadTime = leadValue,
|
|
alertText = textValue,
|
|
}
|
|
end
|
|
|
|
function RT:DispatchEntryAssignments(encounterId, entry, entryIndex, relativeTime)
|
|
local leadTime = tonumber(self:GetSettings().leadTime) or 5
|
|
local dispatchTime = math.max(0, tonumber(relativeTime) or tonumber(entry and entry.time) or 0)
|
|
local assignedPlayers = self:GetAssignedPlayersForEntry(encounterId, entry, entryIndex)
|
|
local alertText = self:GetEntryAlertText(entry) or ""
|
|
for _, assignedPlayer in ipairs(assignedPlayers) do
|
|
HMGT:DebugScoped(
|
|
"info",
|
|
"RaidTimeline",
|
|
"RaidTimeline assign encounter=%s slot=%s time=%s trigger=%s action=%s spellId=%s text=%s player=%s target=%s",
|
|
tostring(encounterId),
|
|
tostring(entryIndex),
|
|
tostring(dispatchTime),
|
|
tostring(self:GetTriggerType(entry)),
|
|
tostring(self:GetActionType(entry)),
|
|
tostring(entry.spellId),
|
|
tostring(alertText),
|
|
tostring(assignedPlayer),
|
|
tostring(entry.targetSpec or entry.playerName or "")
|
|
)
|
|
if NormalizeName(assignedPlayer) == GetOwnName() then
|
|
self:StoreAssignment(encounterId, dispatchTime, entry.spellId, leadTime, alertText)
|
|
else
|
|
local target = self:GetWhisperTargetForPlayer(assignedPlayer)
|
|
if target and target ~= "" then
|
|
HMGT:SendDirectMessage(
|
|
string.format("%s|%d|%d|%d|%d|%s", GetTimelineMessagePrefix(), tonumber(encounterId) or 0, dispatchTime, math.max(0, tonumber(entry.spellId) or 0), leadTime, self:EncodeCommText(alertText)),
|
|
target,
|
|
"ALERT"
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function RT:DispatchAssignmentsForEncounter(encounterId)
|
|
if not self:IsLocalEditor() then
|
|
return
|
|
end
|
|
|
|
local schedule = self:GetEncounter(encounterId)
|
|
if not schedule or type(schedule.entries) ~= "table" then
|
|
return
|
|
end
|
|
|
|
for index, entry in ipairs(schedule.entries) do
|
|
if self:GetTriggerType(entry) == "bossAbility" then
|
|
self:DispatchEntryAssignments(encounterId, entry, index, entry.time)
|
|
end
|
|
end
|
|
end
|
|
|
|
function RT:TryDispatchBossAbilityEntry(entryIndex, entry, castCount, secondsUntilCast)
|
|
if not self:IsLocalEditor() or not self.activeEncounterId or self:GetTriggerType(entry) ~= "bossAbility" then
|
|
return
|
|
end
|
|
local currentCount = math.max(1, tonumber(castCount) or 1)
|
|
local desiredSelector = self:NormalizeCastCountSelector(entry and entry.castCount)
|
|
if not self:DoesCastCountSelectorMatch(desiredSelector, currentCount) then
|
|
return
|
|
end
|
|
local warningLeadTime = tonumber(self:GetSettings().leadTime) or 5
|
|
local secondsRemaining = math.max(0, tonumber(secondsUntilCast) or 0)
|
|
if secondsRemaining > warningLeadTime then
|
|
self:LogBossAbilityProbe(
|
|
entryIndex,
|
|
entry,
|
|
string.format("armed:%s:%d", tostring(desiredSelector), tonumber(currentCount) or 1),
|
|
"RaidTimeline boss ability armed encounter=%s slot=%s selector=%s cast=%s remaining=%.1f lead=%.1f",
|
|
tostring(self.activeEncounterId),
|
|
tostring(entryIndex),
|
|
tostring(self:FormatCastCountSelector(desiredSelector)),
|
|
tostring(currentCount),
|
|
tonumber(secondsRemaining) or 0,
|
|
tonumber(warningLeadTime) or 0
|
|
)
|
|
return
|
|
end
|
|
self.activeBossAbilityDispatches = self.activeBossAbilityDispatches or {}
|
|
local dispatchKey = self:GetBossAbilityDispatchKey(entryIndex, currentCount)
|
|
if self.activeBossAbilityDispatches[dispatchKey] then
|
|
return
|
|
end
|
|
self.activeBossAbilityDispatches[dispatchKey] = true
|
|
local elapsed = math.max(0, GetTime() - (tonumber(self.activeEncounterStartTime) or GetTime()))
|
|
local dispatchTime = elapsed + secondsRemaining
|
|
HMGT:DebugScoped(
|
|
"info",
|
|
"RaidTimeline",
|
|
"RaidTimeline boss ability match encounter=%s slot=%s ability=%s selector=%s cast=%s triggerIn=%.1f lead=%.1f",
|
|
tostring(self.activeEncounterId),
|
|
tostring(entryIndex),
|
|
tostring(entry.bossAbilityId),
|
|
tostring(self:FormatCastCountSelector(desiredSelector)),
|
|
tostring(currentCount),
|
|
tonumber(secondsRemaining) or 0,
|
|
tonumber(warningLeadTime) or 0
|
|
)
|
|
self:DispatchEntryAssignments(self.activeEncounterId, entry, entryIndex, dispatchTime)
|
|
end
|
|
|
|
function RT:HandleAssignmentComm(senderName, encounterId, timeSec, spellId, leadTime, alertText)
|
|
if not self:IsAuthorizedSender(senderName) then
|
|
HMGT:DebugScoped(
|
|
"info",
|
|
"RaidTimeline",
|
|
"RaidTimeline ignored assignment sender=%s encounter=%s time=%s spellId=%s",
|
|
tostring(senderName),
|
|
tostring(encounterId),
|
|
tostring(timeSec),
|
|
tostring(spellId)
|
|
)
|
|
return
|
|
end
|
|
local decodedText = self:DecodeCommText(alertText)
|
|
HMGT:DebugScoped(
|
|
"info",
|
|
"RaidTimeline",
|
|
"RaidTimeline received assignment sender=%s encounter=%s time=%s spellId=%s lead=%s text=%s",
|
|
tostring(senderName),
|
|
tostring(encounterId),
|
|
tostring(timeSec),
|
|
tostring(spellId),
|
|
tostring(leadTime),
|
|
tostring(decodedText)
|
|
)
|
|
self:StoreAssignment(encounterId, timeSec, spellId, leadTime, decodedText)
|
|
end
|
|
|
|
function RT:HandleTestStartComm(senderName, encounterId, difficultyId, serverStartTime, duration)
|
|
if not self:IsAuthorizedSender(senderName) then
|
|
HMGT:DebugScoped(
|
|
"info",
|
|
"RaidTimeline",
|
|
"RaidTimeline ignored test sender=%s encounter=%s",
|
|
tostring(senderName),
|
|
tostring(encounterId)
|
|
)
|
|
return
|
|
end
|
|
|
|
local id = tonumber(encounterId)
|
|
if not id or id <= 0 then
|
|
return
|
|
end
|
|
|
|
self.testActive = true
|
|
self.testEncounterId = id
|
|
self.testDuration = math.max(0, tonumber(duration) or 0)
|
|
self:StartEncounter(id, difficultyId)
|
|
|
|
if tonumber(self.activeEncounterId) ~= id then
|
|
self.testActive = false
|
|
self.testEncounterId = nil
|
|
self.testDuration = nil
|
|
return
|
|
end
|
|
|
|
local serverNow = tonumber(GetServerTime and GetServerTime() or 0) or 0
|
|
local startedAt = tonumber(serverStartTime) or serverNow
|
|
local elapsed = math.max(0, serverNow - startedAt)
|
|
self.activeEncounterStartTime = GetTime() - elapsed
|
|
|
|
HMGT:DebugScoped(
|
|
"info",
|
|
"RaidTimeline",
|
|
"RaidTimeline remote test start sender=%s encounter=%s difficulty=%s elapsed=%.1f duration=%s",
|
|
tostring(senderName),
|
|
tostring(id),
|
|
tostring(GetDifficultyKey(difficultyId) or difficultyId),
|
|
tonumber(elapsed) or 0,
|
|
tostring(self.testDuration)
|
|
)
|
|
end
|
|
|
|
function RT:StartRuntime()
|
|
self.runtimeEnabled = true
|
|
if not self.eventFrame then
|
|
self.eventFrame = CreateFrame("Frame")
|
|
self.eventFrame:SetScript("OnEvent", function(_, event, ...)
|
|
RT:HandleEvent(event, ...)
|
|
end)
|
|
end
|
|
self.eventFrame:RegisterEvent("ENCOUNTER_START")
|
|
self.eventFrame:RegisterEvent("ENCOUNTER_END")
|
|
self.eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
self.eventFrame:RegisterEvent("GROUP_ROSTER_UPDATE")
|
|
if not self.ticker then
|
|
self.ticker = C_Timer.NewTicker(0.1, function()
|
|
RT:HandleDueEntries()
|
|
end)
|
|
end
|
|
self:EnsureAlertFrame()
|
|
self:ApplyAlertStyle()
|
|
self:HandleDueEntries()
|
|
end
|
|
|
|
function RT:StopRuntime()
|
|
self.runtimeEnabled = false
|
|
if self.eventFrame then
|
|
self.eventFrame:UnregisterAllEvents()
|
|
end
|
|
if self.ticker then
|
|
self.ticker:Cancel()
|
|
self.ticker = nil
|
|
end
|
|
self.activeEncounterId = nil
|
|
self.activeEncounterStartTime = nil
|
|
self.activeSchedule = nil
|
|
self.firedEntries = nil
|
|
self.currentAlert = nil
|
|
self.activeBossAbilityDispatches = nil
|
|
self.activeBossAbilityCounts = nil
|
|
self.dispatchedEntries = nil
|
|
self.testActive = false
|
|
self.testEncounterId = nil
|
|
self.testDuration = nil
|
|
if self.alertFrame then
|
|
self.alertFrame:Hide()
|
|
end
|
|
self:CloseTimelineEditor()
|
|
end
|
|
|
|
function RT:OnInitialize()
|
|
HMGT.RaidTimeline = self
|
|
end
|
|
|
|
function RT:OnEnable()
|
|
self:StartRuntime()
|
|
end
|
|
|
|
function RT:OnDisable()
|
|
self:StopRuntime()
|
|
end
|
|
|
|
function RT:StartEncounter(encounterId, difficultyId)
|
|
self.activeEncounterId = tonumber(encounterId)
|
|
self.activeEncounterDifficulty = GetDifficultyKey(difficultyId)
|
|
self.activeEncounterStartTime = GetTime()
|
|
self.activeSchedule = self:GetEncounter(encounterId)
|
|
if self.activeSchedule and not self:IsEncounterEnabledForDifficulty(self.activeSchedule, difficultyId) then
|
|
HMGT:DebugScoped(
|
|
"info",
|
|
"RaidTimeline",
|
|
"RaidTimeline encounter skipped encounter=%s difficulty=%s",
|
|
tostring(self.activeEncounterId),
|
|
tostring(difficultyId)
|
|
)
|
|
self.activeSchedule = nil
|
|
self.activeEncounterId = nil
|
|
self.activeEncounterDifficulty = nil
|
|
self.activeEncounterStartTime = nil
|
|
self.firedEntries = nil
|
|
return
|
|
end
|
|
self.firedEntries = {}
|
|
self.dispatchedEntries = {}
|
|
self.currentAlert = nil
|
|
self.activeBossAbilityDispatches = {}
|
|
self.activeBossAbilityCounts = {}
|
|
self._bossAbilityProbeStates = {}
|
|
self.receivedAssignments = self.receivedAssignments or {}
|
|
self.receivedAssignments[self.activeEncounterId] = {}
|
|
HMGT:DebugScoped(
|
|
"info",
|
|
"RaidTimeline",
|
|
"RaidTimeline encounter start encounter=%s difficulty=%s leader=%s entries=%s",
|
|
tostring(self.activeEncounterId),
|
|
tostring(self.activeEncounterDifficulty or difficultyId),
|
|
tostring(self:IsLocalEditor()),
|
|
tostring(self.activeSchedule and #self.activeSchedule.entries or 0)
|
|
)
|
|
end
|
|
|
|
function RT:StopEncounter()
|
|
self.activeEncounterId = nil
|
|
self.activeEncounterDifficulty = nil
|
|
self.activeEncounterStartTime = nil
|
|
self.activeSchedule = nil
|
|
self.firedEntries = nil
|
|
self.dispatchedEntries = nil
|
|
self.activeBossAbilityDispatches = nil
|
|
self.activeBossAbilityCounts = nil
|
|
self._bossAbilityProbeStates = nil
|
|
self.testActive = false
|
|
self.testEncounterId = nil
|
|
self.testDuration = nil
|
|
self:HideAlert()
|
|
end
|
|
|
|
function RT:GetEncounterTestDuration(encounterId)
|
|
local schedule = self:GetEncounter(encounterId)
|
|
local maxTime = 0
|
|
if schedule and type(schedule.entries) == "table" then
|
|
for _, entry in ipairs(schedule.entries) do
|
|
if self:GetTriggerType(entry) ~= "bossAbility" then
|
|
maxTime = math.max(maxTime, tonumber(entry.time) or 0)
|
|
end
|
|
end
|
|
end
|
|
local leadTime = tonumber(self:GetSettings().leadTime) or 5
|
|
return math.max(15, maxTime + leadTime + 3)
|
|
end
|
|
|
|
function RT:IsTestRunning(encounterId)
|
|
local id = tonumber(encounterId)
|
|
return self.testActive == true and id ~= nil and tonumber(self.testEncounterId) == id
|
|
end
|
|
|
|
function RT:StartEncounterTest(encounterId)
|
|
local id = tonumber(encounterId)
|
|
if not id or id <= 0 then
|
|
return false
|
|
end
|
|
local schedule = self:GetEncounter(id)
|
|
if not schedule then
|
|
return false
|
|
end
|
|
if self.activeEncounterId then
|
|
self:StopEncounter()
|
|
end
|
|
local difficultyId = GetPreferredTestDifficultyId(schedule)
|
|
self.testActive = true
|
|
self.testEncounterId = id
|
|
self.testDuration = self:GetEncounterTestDuration(id)
|
|
HMGT:DebugScoped(
|
|
"info",
|
|
"RaidTimeline",
|
|
"RaidTimeline test start encounter=%s difficulty=%s duration=%s",
|
|
tostring(id),
|
|
tostring(GetDifficultyKey(difficultyId) or difficultyId),
|
|
tostring(self.testDuration)
|
|
)
|
|
self:StartEncounter(id, difficultyId)
|
|
if tonumber(self.activeEncounterId) ~= id then
|
|
self.testActive = false
|
|
self.testEncounterId = nil
|
|
self.testDuration = nil
|
|
return false
|
|
end
|
|
if IsInGroup() or IsInRaid() then
|
|
HMGT:SendGroupMessage(
|
|
string.format(
|
|
"%s|%d|%d|%d|%d",
|
|
GetTimelineTestMessagePrefix(),
|
|
id,
|
|
tonumber(difficultyId) or 0,
|
|
tonumber(GetServerTime and GetServerTime() or 0) or 0,
|
|
tonumber(self.testDuration) or 0
|
|
),
|
|
"ALERT"
|
|
)
|
|
end
|
|
self:Refresh()
|
|
return true
|
|
end
|
|
|
|
function RT:StopEncounterTest()
|
|
if self.testActive then
|
|
HMGT:DebugScoped(
|
|
"info",
|
|
"RaidTimeline",
|
|
"RaidTimeline test stop encounter=%s",
|
|
tostring(self.testEncounterId or self.activeEncounterId)
|
|
)
|
|
end
|
|
self:StopEncounter()
|
|
end
|
|
|
|
function RT:HandleDueEntries()
|
|
if not self.runtimeEnabled or (self:GetSettings().enabled ~= true and not self.testActive) then
|
|
self:HideAlert()
|
|
return
|
|
end
|
|
if self.previewAlertActive == true then
|
|
self:ApplyAlertStyle()
|
|
return
|
|
end
|
|
if not self.activeEncounterId or not self.activeEncounterStartTime then
|
|
self:UpdateCurrentAlert()
|
|
return
|
|
end
|
|
|
|
local assignments = self.receivedAssignments and self.receivedAssignments[self.activeEncounterId]
|
|
if type(assignments) ~= "table" then
|
|
self:UpdateCurrentAlert()
|
|
return
|
|
end
|
|
|
|
local elapsed = GetTime() - self.activeEncounterStartTime
|
|
if self:IsLocalEditor() and self.activeSchedule and type(self.activeSchedule.entries) == "table" then
|
|
self.dispatchedEntries = self.dispatchedEntries or {}
|
|
local assignmentLeadTime = tonumber(self:GetSettings().assignmentLeadTime) or tonumber(self:GetSettings().leadTime) or 5
|
|
for index, entry in ipairs(self.activeSchedule.entries) do
|
|
if self:GetTriggerType(entry) == "bossAbility" then
|
|
self:ProbeBossAbilityEntry(index, entry)
|
|
elseif self.dispatchedEntries[index] ~= true then
|
|
local assignAt = math.max(0, (tonumber(entry.time) or 0) - assignmentLeadTime)
|
|
if elapsed >= assignAt then
|
|
self.dispatchedEntries[index] = true
|
|
HMGT:DebugScoped(
|
|
"verbose",
|
|
"RaidTimeline",
|
|
"RaidTimeline assignment due encounter=%s slot=%s time=%s assignAt=%.1f elapsed=%.1f",
|
|
tostring(self.activeEncounterId),
|
|
tostring(index),
|
|
tostring(entry.time),
|
|
tonumber(assignAt) or 0,
|
|
tonumber(elapsed) or 0
|
|
)
|
|
self:DispatchEntryAssignments(self.activeEncounterId, entry, index, entry.time)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
for key, assignment in pairs(assignments) do
|
|
if self.firedEntries[key] ~= true then
|
|
local warnAt = math.max(0, (tonumber(assignment.time) or 0) - (tonumber(assignment.leadTime) or 5))
|
|
if elapsed >= warnAt then
|
|
self.firedEntries[key] = true
|
|
HMGT:DebugScoped(
|
|
"info",
|
|
"RaidTimeline",
|
|
"RaidTimeline firing encounter=%s key=%s time=%s spellId=%s elapsed=%.1f",
|
|
tostring(self.activeEncounterId),
|
|
tostring(key),
|
|
tostring(assignment.time),
|
|
tostring(assignment.spellId),
|
|
tonumber(elapsed) or 0
|
|
)
|
|
local countdown = math.max(0, (tonumber(assignment.time) or 0) - elapsed)
|
|
self:ShowCountdownAlert(assignment.spellId, countdown, assignment.alertText)
|
|
end
|
|
end
|
|
end
|
|
self:UpdateCurrentAlert()
|
|
if self.testActive then
|
|
local duration = tonumber(self.testDuration) or 0
|
|
if duration > 0 and elapsed >= duration and not self.currentAlert then
|
|
HMGT:DebugScoped(
|
|
"info",
|
|
"RaidTimeline",
|
|
"RaidTimeline test complete encounter=%s elapsed=%.1f",
|
|
tostring(self.activeEncounterId),
|
|
tonumber(elapsed) or 0
|
|
)
|
|
self:StopEncounter()
|
|
end
|
|
end
|
|
end
|
|
|
|
function RT:Refresh()
|
|
self:ApplyAlertStyle()
|
|
self:HandleDueEntries()
|
|
self:RefreshTimelineEditor()
|
|
end
|
|
|
|
function RT:HandleEvent(event, ...)
|
|
if event == "ENCOUNTER_START" then
|
|
local encounterId, _, difficultyId = ...
|
|
self:StartEncounter(encounterId, difficultyId)
|
|
elseif event == "ENCOUNTER_END" or event == "PLAYER_ENTERING_WORLD" then
|
|
self:StopEncounter()
|
|
elseif event == "GROUP_ROSTER_UPDATE" then
|
|
self:HandleDueEntries()
|
|
end
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|