initial commit

This commit is contained in:
Torsten Brendgen
2026-04-10 21:30:31 +02:00
commit fc5a8aa361
108 changed files with 40568 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,202 @@
local ADDON_NAME = "HailMaryGuildTools"
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
if not HMGT then return end
local RT = HMGT.RaidTimeline
if not RT then return end
local function TrimText(value)
local text = tostring(value or "")
text = string.gsub(text, "^%s+", "")
text = string.gsub(text, "%s+$", "")
return text
end
local function StripBarDisplayText(value)
local text = tostring(value or "")
text = string.gsub(text, "|T.-|t", "")
text = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
text = string.gsub(text, "|r", "")
return TrimText(text)
end
local function NormalizeBossAbilityBarName(value)
local text = TrimText(value)
if text == "" then
return ""
end
text = string.gsub(text, "%s*%(%d+%)%s*$", "")
text = string.gsub(text, "%s*%([Cc]ount%)%s*$", "")
text = string.gsub(text, "^%s*%[([A-Za-z])%]%s*", "%1 ")
text = string.gsub(text, "%s*%[([A-Za-z])%]%s*$", " (%1)")
text = string.gsub(text, "%s*%(([A-Za-z])%)%s*$", " (%1)")
text = string.lower(text)
return TrimText(text)
end
local function EnsureBigWigsBridge()
if RT._bigWigsBridgeRegistered == true then
return true
end
if type(BigWigsLoader) ~= "table" or type(BigWigsLoader.RegisterMessage) ~= "function" then
return false
end
RT._bigWigsObservedBars = RT._bigWigsObservedBars or setmetatable({}, { __mode = "k" })
RT._bigWigsReceiver = RT._bigWigsReceiver or {}
function RT._bigWigsReceiver:OnBigWigsBarCreated(_, plugin, bar, module, key, text, time)
RT._bigWigsObservedBars[bar] = {
plugin = plugin,
module = module,
key = key,
createdText = tostring(text or ""),
duration = tonumber(time) or 0,
}
end
function RT._bigWigsReceiver:OnBigWigsStopBar(_, plugin, module, text)
local targetText = StripBarDisplayText(text)
for bar, info in pairs(RT._bigWigsObservedBars) do
local currentText = ""
if type(bar) == "table" and type(bar.GetText) == "function" then
currentText = StripBarDisplayText(bar:GetText())
end
if (info and info.plugin == plugin or not info or not info.plugin)
and (currentText == targetText or StripBarDisplayText(info and info.createdText) == targetText) then
RT._bigWigsObservedBars[bar] = nil
end
end
end
BigWigsLoader.RegisterMessage(RT._bigWigsReceiver, "BigWigs_BarCreated", "OnBigWigsBarCreated")
BigWigsLoader.RegisterMessage(RT._bigWigsReceiver, "BigWigs_StopBar", "OnBigWigsStopBar")
RT._bigWigsBridgeRegistered = true
return true
end
function RT:GetObservedBigWigsBars()
local observed = {}
if not EnsureBigWigsBridge() then
return observed
end
for bar, info in pairs(self._bigWigsObservedBars or {}) do
local rawText = nil
if type(bar) == "table" and type(bar.GetText) == "function" then
rawText = bar:GetText()
end
rawText = StripBarDisplayText(rawText or (info and info.createdText) or "")
local remaining = math.max(0, tonumber(bar and bar.remaining) or 0)
if rawText ~= "" and remaining > 0 then
observed[#observed + 1] = {
rawText = rawText,
normalizedName = NormalizeBossAbilityBarName(rawText),
count = tonumber(string.match(rawText, "%((%d+)%)%s*$")) or 1,
seconds = remaining,
key = info and info.key or nil,
}
else
self._bigWigsObservedBars[bar] = nil
end
end
return observed
end
function RT:ProbeBossAbilityEntry(entryIndex, entry)
if not self:IsLocalEditor() or not self.activeEncounterId or self:GetTriggerType(entry) ~= "bossAbility" then
return
end
local desiredRawName = TrimText(entry and entry.bossAbilityBarName or "")
local desiredName = NormalizeBossAbilityBarName(desiredRawName)
local desiredSelector = self:NormalizeCastCountSelector(entry and entry.castCount)
local desiredSelectorText = self:FormatCastCountSelector(desiredSelector)
if desiredName == "" then
self:LogBossAbilityProbe(
entryIndex,
entry,
"missing-bar-name",
"RaidTimeline BigWigs probe encounter=%s slot=%s skipped=missing-bar-name",
tostring(self.activeEncounterId),
tostring(entryIndex)
)
return
end
local bars = self:GetObservedBigWigsBars()
if #bars == 0 then
self:LogBossAbilityProbe(
entryIndex,
entry,
"no-bars",
"RaidTimeline BigWigs probe encounter=%s slot=%s target=%s normalized=%s selector=%s foundBar=false bars=0",
tostring(self.activeEncounterId),
tostring(entryIndex),
tostring(desiredRawName),
tostring(desiredName),
tostring(desiredSelectorText)
)
return
end
local seenNames = {}
local foundName = false
for _, observed in ipairs(bars) do
seenNames[#seenNames + 1] = string.format("%s{key=%s}[%d]=%.1fs", tostring(observed.rawText), tostring(observed.key), tonumber(observed.count) or 1, tonumber(observed.seconds) or 0)
if observed.normalizedName == desiredName then
foundName = true
if self:DoesCastCountSelectorMatch(desiredSelector, observed.count) then
self:LogBossAbilityProbe(
entryIndex,
entry,
string.format("match:%s:%s:%d", desiredName, tostring(desiredSelector), tonumber(observed.count) or 1),
"RaidTimeline BigWigs probe encounter=%s slot=%s target=%s normalized=%s selector=%s foundBar=true countMatch=true observed=%s key=%s observedCount=%d remaining=%.1f",
tostring(self.activeEncounterId),
tostring(entryIndex),
tostring(desiredRawName),
tostring(desiredName),
tostring(desiredSelectorText),
tostring(observed.rawText),
tostring(observed.key),
tonumber(observed.count) or 1,
tonumber(observed.seconds) or 0
)
self:TryDispatchBossAbilityEntry(entryIndex, entry, observed.count, observed.seconds)
return
end
self:LogBossAbilityProbe(
entryIndex,
entry,
string.format("count-mismatch:%s:%d", desiredName, observed.count),
"RaidTimeline BigWigs probe encounter=%s slot=%s target=%s normalized=%s selector=%s foundBar=true countMatch=false observed=%s key=%s observedCount=%d remaining=%.1f",
tostring(self.activeEncounterId),
tostring(entryIndex),
tostring(desiredRawName),
tostring(desiredName),
tostring(desiredSelectorText),
tostring(observed.rawText),
tostring(observed.key),
tonumber(observed.count) or 1,
tonumber(observed.seconds) or 0
)
end
end
if not foundName then
self:LogBossAbilityProbe(
entryIndex,
entry,
string.format("name-miss:%s", desiredName),
"RaidTimeline BigWigs probe encounter=%s slot=%s target=%s normalized=%s selector=%s foundBar=false bars=%s",
tostring(self.activeEncounterId),
tostring(entryIndex),
tostring(desiredRawName),
tostring(desiredName),
tostring(desiredSelectorText),
table.concat(seenNames, ", ")
)
end
end

View File

@@ -0,0 +1,52 @@
local ADDON_NAME = "HailMaryGuildTools"
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
if not HMGT then return end
HMGT.RaidTimelineBossAbilityData = HMGT.RaidTimelineBossAbilityData or {
raids = {
--[[
{
name = "Void Spire",
journalInstanceId = 1307,
bosses = {
[3176] = {
name = "Imperator Averzian",
abilities = {
{
key = "gloom",
name = "Gloom",
spellId = 123456,
icon = 1234567,
difficulties = {
lfr = false,
normal = true,
heroic = true,
mythic = true,
},
triggers = {
bigwigs = { 123456, "gloom" },
dbm = { 123456 },
},
},
{
key = "mythic_gloom",
name = "Mythic Gloom",
spellId = 123457,
difficulties = {
lfr = false,
normal = false,
heroic = false,
mythic = true,
},
triggers = {
bigwigs = { 123457 },
dbm = { 123457 },
},
},
},
},
},
},
]]
},
}

View File

@@ -0,0 +1,8 @@
local ADDON_NAME = "HailMaryGuildTools"
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
if not HMGT then return end
local RT = HMGT.RaidTimeline
if not RT then return end
-- Placeholder for later DBM-specific raid timeline integration.

View File

@@ -0,0 +1,799 @@
local ADDON_NAME = "HailMaryGuildTools"
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
if not HMGT then return end
local RT = HMGT.RaidTimeline
if not RT then return end
if not HMGT_Config or not HMGT_Config.RegisterOptionsProvider then return end
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME, true) or {}
local AceConfigRegistry = LibStub("AceConfigRegistry-3.0")
local LSM = LibStub("LibSharedMedia-3.0", true)
local FONT_OUTLINE_VALUES = {
NONE = NONE or "None",
OUTLINE = "Outline",
THICKOUTLINE = "Thick Outline",
MONOCHROME = "Monochrome",
["OUTLINE,MONOCHROME"] = "Outline Monochrome",
}
local DIFFICULTY_KEYS = { "lfr", "normal", "heroic", "mythic" }
local MAX_ENTRY_ROWS = 24
local raidTimelineOptionsGroup
local raidCooldownSpellValuesCache
local bossAbilityValuesCache = {}
local function ClearOptionCaches()
raidCooldownSpellValuesCache = nil
bossAbilityValuesCache = {}
end
local function Notify(rebuild)
if rebuild then
ClearOptionCaches()
end
if rebuild and raidTimelineOptionsGroup then
local fresh = RT:GetOptionsGroup()
raidTimelineOptionsGroup.name = fresh.name
raidTimelineOptionsGroup.order = fresh.order
raidTimelineOptionsGroup.childGroups = fresh.childGroups
raidTimelineOptionsGroup.args = fresh.args
end
AceConfigRegistry:NotifyChange(ADDON_NAME)
end
local function GetDrafts()
HMGT._raidTimelineDraft = HMGT._raidTimelineDraft or {
addEncounterId = "",
addEncounterName = "",
entries = {},
}
HMGT._raidTimelineDraft.entries = HMGT._raidTimelineDraft.entries or {}
return HMGT._raidTimelineDraft
end
local function TrimText(value)
return tostring(value or ""):gsub("^%s+", ""):gsub("%s+$", "")
end
local function GetEncounterDraft(encounterId)
local drafts = GetDrafts()
local key = tostring(tonumber(encounterId) or encounterId or "")
drafts.entries[key] = drafts.entries[key] or {
time = "",
spellId = 0,
alertText = "",
playerName = "",
entryType = "spell",
triggerType = "time",
actionType = "raidCooldown",
targetSpec = "",
bossAbilityId = "",
bossAbilityBarName = "",
castCount = "1",
}
return drafts.entries[key]
end
local function GetTriggerTypeValues()
return (RT.GetTriggerTypeValues and RT:GetTriggerTypeValues()) or {
time = L["OPT_RT_TRIGGER_TIME"] or "Time",
bossAbility = L["OPT_RT_TRIGGER_BOSS_ABILITY"] or "Boss ability",
}
end
local function GetActionTypeValues()
return (RT.GetActionTypeValues and RT:GetActionTypeValues()) or {
text = L["OPT_RT_ACTION_TEXT"] or "Text",
raidCooldown = L["OPT_RT_ACTION_RAID_COOLDOWN"] or "Raid Cooldown",
}
end
local function GetEncounterEntry(encounterId, row)
local encounter = encounterId and RT:GetEncounter(encounterId)
return encounter and encounter.entries and encounter.entries[row] or nil
end
local function GetTargetFieldLabel(kind, isAddRow)
return (isAddRow and (L["OPT_RT_ADD_PLAYER"] or "Target")) or (L["OPT_RT_ENTRY_PLAYER"] or "Target")
end
local function GetTargetFieldDesc(kind)
return L["OPT_RT_ADD_PLAYER_DESC"] or "Optional. Comma-separated player names or variables like Group1, Group8, GroupEven, GroupOdd."
end
local function FormatEntryTime(value)
return RT.FormatTimelineClock and RT:FormatTimelineClock(value) or tostring(value or "")
end
local function GetBossAbilityValues(encounterId)
local encounterKey = tostring(tonumber(encounterId) or encounterId or 0)
if bossAbilityValuesCache[encounterKey] then
return bossAbilityValuesCache[encounterKey]
end
local values = RT.GetBossAbilityValues and RT:GetBossAbilityValues(encounterId) or { [""] = L["OPT_RT_NO_BOSS_ABILITY"] or "No boss ability" }
bossAbilityValuesCache[encounterKey] = values
return values
end
local function GetDraftTriggerType(encounterId)
local draft = GetEncounterDraft(encounterId)
draft.triggerType = tostring(draft.triggerType or "time")
return draft.triggerType == "bossAbility" and "bossAbility" or "time"
end
local function GetDraftActionType(encounterId)
local draft = GetEncounterDraft(encounterId)
draft.actionType = tostring(draft.actionType or "raidCooldown")
return draft.actionType == "text" and "text" or "raidCooldown"
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 GetSpellIcon(spellId)
local sid = tonumber(spellId)
if not sid or sid <= 0 then return nil end
if HMGT_SpellData and type(HMGT_SpellData.GetSpellIcon) == "function" then
local icon = HMGT_SpellData.GetSpellIcon(sid)
if icon and icon ~= "" then
return icon
end
end
if C_Spell and type(C_Spell.GetSpellTexture) == "function" then
local icon = C_Spell.GetSpellTexture(sid)
if icon and icon ~= "" then
return icon
end
end
local _, _, icon = GetSpellInfo(sid)
return icon
end
local function GetRaidCooldownSpellValues()
if raidCooldownSpellValuesCache then
return raidCooldownSpellValuesCache
end
local values = { [0] = L["OPT_RT_NO_SPELL"] or "No spell" }
local seen = { [0] = true }
for _, entry in ipairs(HMGT_SpellData and HMGT_SpellData.RaidCooldowns or {}) 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 = GetSpellIcon(spellId)
values[spellId] = icon and icon ~= ""
and string.format("|T%s:16:16:0:0|t %s (%d)", tostring(icon), spellName, spellId)
or string.format("%s (%d)", spellName, spellId)
end
end
raidCooldownSpellValuesCache = values
return values
end
local function GetDifficultyLabel(key)
if key == "lfr" then return L["OPT_RT_DIFF_LFR"] or "LFR" end
if key == "heroic" then return L["OPT_RT_DIFF_HEROIC"] or "HC" end
if key == "mythic" then return L["OPT_RT_DIFF_MYTHIC"] or "Mythic" end
return L["OPT_RT_DIFF_NORMAL"] or "Normal"
end
local function GetEncounterLabel(encounterId)
local encounter = RT:GetEncounter(encounterId)
local name = TrimText(encounter and encounter.name or "")
if name == "" then
name = L["OPT_RT_ENCOUNTER"] or "Encounter"
end
return string.format("%s (%d)", name, tonumber(encounterId) or 0)
end
local function GetBossTreeLabel(encounterId)
local encounter = RT:GetEncounter(encounterId)
local name = TrimText(encounter and encounter.name or "")
if name == "" then
name = L["OPT_RT_ENCOUNTER"] or "Encounter"
end
return string.format("%s (%d)", name, #(encounter and encounter.entries or {}))
end
local function GetRaidBuckets()
local buckets, raidNames = {}, {}
for _, encounterId in ipairs(RT:GetEncounterIds()) do
local encounter = RT:GetEncounter(encounterId)
if encounter then
local journalInstanceId, instanceName = RT:GetEncounterInstanceInfo(encounterId)
local raidName = tostring(encounter.instanceName or instanceName or (L["OPT_RT_RAID_DEFAULT"] or "Encounter"))
if not buckets[raidName] then
buckets[raidName] = {
key = tostring(journalInstanceId or raidName),
ids = {},
difficulties = { lfr = {}, normal = {}, heroic = {}, mythic = {} },
}
table.insert(raidNames, raidName)
end
table.insert(buckets[raidName].ids, encounterId)
for _, difficultyKey in ipairs(DIFFICULTY_KEYS) do
if encounter.difficulties and encounter.difficulties[difficultyKey] == true then
table.insert(buckets[raidName].difficulties[difficultyKey], encounterId)
end
end
end
end
table.sort(raidNames)
for _, raidName in ipairs(raidNames) do
table.sort(buckets[raidName].ids)
for _, difficultyKey in ipairs(DIFFICULTY_KEYS) do
table.sort(buckets[raidName].difficulties[difficultyKey])
end
end
return raidNames, buckets
end
local function BuildEntryEditorArgs(encounterId)
local args = {
entriesHeader = {
type = "header",
order = 20,
name = L["OPT_RT_ENCOUNTERS_HEADER"] or "Encounter timelines",
},
addTime = {
type = "input", order = 21, width = 0.7, name = L["OPT_RT_ADD_TIME"] or "Time (MM:SS)",
disabled = function() return not RT:IsLocalEditor() end,
get = function() return tostring(GetEncounterDraft(encounterId).time or "") end,
set = function(_, val) GetEncounterDraft(encounterId).time = val end,
hidden = function() return GetDraftTriggerType(encounterId) == "bossAbility" end,
},
addTriggerType = {
type = "select", order = 22, width = 0.8, name = L["OPT_RT_TRIGGER"] or "Trigger",
disabled = function() return not RT:IsLocalEditor() end,
values = GetTriggerTypeValues,
get = function() return GetDraftTriggerType(encounterId) end,
set = function(_, val)
local draft = GetEncounterDraft(encounterId)
draft.triggerType = (tostring(val or "") == "bossAbility") and "bossAbility" or "time"
if draft.triggerType == "time" then
draft.bossAbilityId = ""
draft.bossAbilityBarName = ""
draft.castCount = "1"
end
Notify(false)
end,
},
addActionType = {
type = "select", order = 22.1, width = 1.0, name = L["OPT_RT_ACTION"] or "Action",
disabled = function() return not RT:IsLocalEditor() end,
values = GetActionTypeValues,
get = function() return GetDraftActionType(encounterId) end,
set = function(_, val)
local draft = GetEncounterDraft(encounterId)
draft.actionType = (tostring(val or "") == "text") and "text" or "raidCooldown"
if draft.actionType == "text" then
draft.spellId = 0
end
Notify(false)
end,
},
addSpellId = {
type = "select", order = 23, width = 1.2, name = L["OPT_RT_ADD_SPELL"] or "Spell",
disabled = function() return not RT:IsLocalEditor() end,
values = GetRaidCooldownSpellValues,
get = function() return math.max(0, tonumber(GetEncounterDraft(encounterId).spellId) or 0) end,
set = function(_, val) GetEncounterDraft(encounterId).spellId = math.max(0, tonumber(val) or 0) end,
hidden = function() return GetDraftActionType(encounterId) ~= "raidCooldown" end,
},
addCastCount = {
type = "input", order = 23.1, width = 0.7, name = L["OPT_RT_CAST_COUNT"] or "Cast count",
disabled = function() return not RT:IsLocalEditor() end,
desc = L["OPT_RT_CAST_COUNT_DESC"] or "Use a number, All, Odd, or Even.",
get = function() return RT:FormatCastCountSelector(GetEncounterDraft(encounterId).castCount or "1") end,
set = function(_, val) GetEncounterDraft(encounterId).castCount = RT:NormalizeCastCountSelector(val) end,
hidden = function() return GetDraftTriggerType(encounterId) ~= "bossAbility" end,
},
addBossAbilityBarName = {
type = "input", order = 23.2, width = 1.2, name = L["OPT_RT_BOSS_BAR_NAME"] or "Bossmod bar name",
disabled = function() return not RT:IsLocalEditor() end,
get = function() return tostring(GetEncounterDraft(encounterId).bossAbilityBarName or "") end,
set = function(_, val) GetEncounterDraft(encounterId).bossAbilityBarName = tostring(val or "") end,
hidden = function() return GetDraftTriggerType(encounterId) ~= "bossAbility" end,
},
addAlertText = {
type = "input", order = 24, width = 1.2, name = L["OPT_RT_ADD_TEXT"] or "Custom text",
disabled = function() return not RT:IsLocalEditor() end,
get = function() return tostring(GetEncounterDraft(encounterId).alertText or "") end,
set = function(_, val) GetEncounterDraft(encounterId).alertText = tostring(val or "") end,
hidden = function()
return GetDraftActionType(encounterId) ~= "text"
end,
},
addPlayerName = {
type = "input", order = 25, width = 1.2,
name = function() return GetTargetFieldLabel(GetDraftActionType(encounterId), true) end,
desc = function() return GetTargetFieldDesc(GetDraftActionType(encounterId)) end,
disabled = function() return not RT:IsLocalEditor() end,
get = function()
local draft = GetEncounterDraft(encounterId)
return tostring(draft.playerName or draft.targetSpec or "")
end,
set = function(_, val)
local draft = GetEncounterDraft(encounterId)
draft.playerName = tostring(val or "")
draft.targetSpec = tostring(val or "")
end,
},
addEntry = {
type = "execute", order = 26, width = "full", name = L["OPT_RT_ADD_ENTRY"] or "Add entry",
disabled = function() return not RT:IsLocalEditor() end,
func = function()
local draft = GetEncounterDraft(encounterId)
local ok = RT:AddDetailedEntry(encounterId, draft)
if ok then
draft.time, draft.spellId, draft.alertText, draft.playerName, draft.entryType, draft.triggerType, draft.actionType, draft.targetSpec, draft.bossAbilityId, draft.bossAbilityBarName, draft.castCount = "", 0, "", "", "spell", "time", "raidCooldown", "", "", "", "1"
Notify(true)
else
HMGT:Print(L["OPT_RT_ADD_ENTRY_INVALID"] or "HMGT: invalid raid timeline entry")
end
end,
},
addBreak = { type = "description", order = 27, width = "full", name = " " },
}
for entryRow = 1, MAX_ENTRY_ROWS do
local order = 40 + (entryRow * 10)
args["entryTime_" .. entryRow] = {
type = "input", order = order, width = 0.7,
disabled = function() return not RT:IsLocalEditor() end,
name = function() return entryRow == 1 and (L["OPT_RT_ENTRY_TIME"] or "Time") or "" end,
get = function() local entry = GetEncounterEntry(encounterId, entryRow); return entry and FormatEntryTime(entry.time or 0) or "" end,
set = function(_, val)
if not RT:SetEntryField(encounterId, entryRow, "time", val) then
HMGT:Print(L["OPT_RT_INVALID_TIME"] or "HMGT: invalid time")
end
Notify(false)
end,
hidden = function()
local entry = GetEncounterEntry(encounterId, entryRow)
return not entry or RT:GetTriggerType(entry) == "bossAbility"
end,
}
args["entryTriggerType_" .. entryRow] = {
type = "select", order = order + 1, width = 0.8,
disabled = function() return not RT:IsLocalEditor() end,
name = function() return entryRow == 1 and (L["OPT_RT_TRIGGER"] or "Trigger") or "" end,
values = GetTriggerTypeValues,
get = function() local entry = GetEncounterEntry(encounterId, entryRow); return entry and RT:GetTriggerType(entry) or "time" end,
set = function(_, val) RT:SetEntryField(encounterId, entryRow, "triggerType", val); Notify(false) end,
hidden = function() return GetEncounterEntry(encounterId, entryRow) == nil end,
}
args["entryActionType_" .. entryRow] = {
type = "select", order = order + 1.1, width = 1.0,
disabled = function() return not RT:IsLocalEditor() end,
name = function() return entryRow == 1 and (L["OPT_RT_ACTION"] or "Action") or "" end,
values = GetActionTypeValues,
get = function() local entry = GetEncounterEntry(encounterId, entryRow); return entry and RT:GetActionType(entry) or "raidCooldown" end,
set = function(_, val) RT:SetEntryField(encounterId, entryRow, "actionType", val); Notify(false) end,
hidden = function() return GetEncounterEntry(encounterId, entryRow) == nil end,
}
args["entrySpell_" .. entryRow] = {
type = "select", order = order + 2, width = 1.2,
disabled = function() return not RT:IsLocalEditor() end,
name = function() return entryRow == 1 and (L["OPT_RT_ENTRY_SPELL"] or "Spell") or "" end,
values = GetRaidCooldownSpellValues,
get = function() local entry = GetEncounterEntry(encounterId, entryRow); return entry and math.max(0, tonumber(entry.spellId) or 0) or 0 end,
set = function(_, val)
if not RT:SetEntryField(encounterId, entryRow, "spellId", math.max(0, tonumber(val) or 0)) then
HMGT:Print(L["OPT_RT_INVALID_SPELL"] or "HMGT: invalid spell ID")
end
Notify(false)
end,
hidden = function() local entry = GetEncounterEntry(encounterId, entryRow); return not entry or RT:GetActionType(entry) ~= "raidCooldown" end,
}
args["entryCastCount_" .. entryRow] = {
type = "input", order = order + 2.1, width = 0.7,
disabled = function() return not RT:IsLocalEditor() end,
name = function() return entryRow == 1 and (L["OPT_RT_CAST_COUNT"] or "Cast count") or "" end,
desc = L["OPT_RT_CAST_COUNT_DESC"] or "Use a number, All, Odd, or Even.",
get = function() local entry = GetEncounterEntry(encounterId, entryRow); return entry and RT:FormatCastCountSelector(entry.castCount) or "1" end,
set = function(_, val) RT:SetEntryField(encounterId, entryRow, "castCount", val); Notify(false) end,
hidden = function() local entry = GetEncounterEntry(encounterId, entryRow); return not entry or RT:GetTriggerType(entry) ~= "bossAbility" end,
}
args["entryBossAbilityBarName_" .. entryRow] = {
type = "input", order = order + 2.2, width = 1.2,
disabled = function() return not RT:IsLocalEditor() end,
name = function() return entryRow == 1 and (L["OPT_RT_BOSS_BAR_NAME"] or "Bossmod bar name") or "" end,
get = function() local entry = GetEncounterEntry(encounterId, entryRow); return entry and tostring(entry.bossAbilityBarName or "") or "" end,
set = function(_, val) RT:SetEntryField(encounterId, entryRow, "bossAbilityBarName", val); Notify(false) end,
hidden = function() local entry = GetEncounterEntry(encounterId, entryRow); return not entry or RT:GetTriggerType(entry) ~= "bossAbility" end,
}
args["entryText_" .. entryRow] = {
type = "input", order = order + 3, width = 1.2,
disabled = function() return not RT:IsLocalEditor() end,
name = function() return entryRow == 1 and (L["OPT_RT_ENTRY_TEXT"] or "Custom text") or "" end,
get = function() local entry = GetEncounterEntry(encounterId, entryRow); return entry and tostring(entry.alertText or "") or "" end,
set = function(_, val) RT:SetEntryField(encounterId, entryRow, "alertText", val); Notify(false) end,
hidden = function()
local entry = GetEncounterEntry(encounterId, entryRow)
if not entry then
return true
end
return RT:GetActionType(entry) ~= "text"
end,
}
args["entryPlayer_" .. entryRow] = {
type = "input", order = order + 4, width = 1.1,
disabled = function() return not RT:IsLocalEditor() end,
name = function()
local entry = GetEncounterEntry(encounterId, entryRow)
return entryRow == 1 and GetTargetFieldLabel(entry and RT:GetActionType(entry) or "raidCooldown", false) or ""
end,
desc = function()
local entry = GetEncounterEntry(encounterId, entryRow)
return GetTargetFieldDesc(entry and RT:GetActionType(entry) or "raidCooldown")
end,
get = function()
local entry = GetEncounterEntry(encounterId, entryRow)
if not entry then return "" end
return tostring(entry.playerName or entry.targetSpec or "")
end,
set = function(_, val)
local entry = GetEncounterEntry(encounterId, entryRow)
if entry then
RT:SetEntryField(encounterId, entryRow, "playerName", val)
RT:SetEntryField(encounterId, entryRow, "targetSpec", val)
Notify(false)
end
end,
hidden = function() return GetEncounterEntry(encounterId, entryRow) == nil end,
}
args["entryDelete_" .. entryRow] = {
type = "execute", order = order + 5, width = 0.6, name = REMOVE or "Remove",
disabled = function() return not RT:IsLocalEditor() end,
func = function() RT:RemoveEntry(encounterId, entryRow); Notify(true) end,
hidden = function() return GetEncounterEntry(encounterId, entryRow) == nil end,
}
args["entryBreak_" .. entryRow] = {
type = "description", order = order + 6, width = "full", name = " ",
hidden = function() return GetEncounterEntry(encounterId, entryRow) == nil end,
}
end
return args
end
local function BuildEncounterDetailGroup(encounterId)
local encounter = RT:GetEncounter(encounterId)
if not encounter then return nil end
local args = {
header = { type = "header", order = 1, name = GetEncounterLabel(encounterId) },
encounterId = {
type = "description", order = 2, width = "full",
name = string.format("|cffffd100%s|r: %d", L["OPT_RT_ADD_ENCOUNTER_ID"] or "Encounter ID", tonumber(encounterId) or 0),
},
raidName = {
type = "description", order = 3, width = "full",
name = function()
local _, instanceName = RT:GetEncounterInstanceInfo(encounterId)
local current = RT:GetEncounter(encounterId)
return string.format("|cffffd100%s|r: %s", L["OPT_RT_RAID_NAME"] or "Raid", tostring((current and current.instanceName) or instanceName or (L["OPT_RT_RAID_DEFAULT"] or "Encounter")))
end,
},
encounterName = {
type = "input", order = 4, width = "full", name = L["OPT_RT_ENCOUNTER_NAME"] or "Name",
disabled = function() return not RT:IsLocalEditor() end,
get = function() local current = RT:GetEncounter(encounterId); return current and tostring(current.name or "") or "" end,
set = function(_, val) local current = RT:GetEncounter(encounterId); if current then current.name = tostring(val or ""); Notify(true) end end,
},
difficultyHeader = { type = "header", order = 5, name = L["OPT_RT_DIFFICULTY_HEADER"] or "Difficulties" },
}
local diffOrder = 6
for _, difficultyKey in ipairs(DIFFICULTY_KEYS) do
args["difficulty_" .. difficultyKey] = {
type = "toggle", order = diffOrder, width = 0.75, name = GetDifficultyLabel(difficultyKey),
disabled = function() return not RT:IsLocalEditor() end,
get = function()
local current = RT:GetEncounter(encounterId)
local difficulties = current and current.difficulties or nil
return difficulties and difficulties[difficultyKey] ~= false or false
end,
set = function(_, val)
local current = RT:GetEncounter(encounterId)
if current then
current.difficulties = current.difficulties or {}
current.difficulties[difficultyKey] = val and true or false
Notify(true)
end
end,
}
diffOrder = diffOrder + 0.01
end
args.openTimelineEditor = {
type = "execute", order = 7, width = "full", name = L["OPT_RT_OPEN_EDITOR"] or "Open timeline",
func = function() if RT.OpenTimelineEditor then RT:OpenTimelineEditor(encounterId) end end,
}
args.runTest = {
type = "execute",
order = 7.5,
width = "full",
name = function()
if RT:IsTestRunning(encounterId) then
return L["OPT_RT_STOP_TEST"] or "Stop test"
end
return L["OPT_RT_START_TEST"] or "Start timeline test"
end,
disabled = function() return not RT:IsLocalEditor() end,
func = function()
if RT:IsTestRunning(encounterId) then
RT:StopEncounterTest()
else
RT:StartEncounterTest(encounterId)
end
Notify()
end,
}
args.testHint = {
type = "description",
order = 7.6,
width = "full",
name = L["OPT_RT_TEST_HINT"] or "Runs the encounter timeline outside of combat so you can verify assignments, whispers and debug output.",
}
args.editorHint = {
type = "description", order = 8, width = "full",
name = 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.",
}
args.encounterDelete = {
type = "execute", order = 9, width = "full", name = DELETE or "Delete",
disabled = function() return not RT:IsLocalEditor() end,
confirm = function() return string.format(L["OPT_RT_DELETE_ENCOUNTER_CONFIRM"] or "Delete raid timeline for encounter %d?", tonumber(encounterId) or 0) end,
func = function() RT:RemoveEncounter(encounterId); Notify(true) end,
}
args.entryBreak = { type = "description", order = 10, width = "full", name = " " }
for key, value in pairs(BuildEntryEditorArgs(encounterId)) do
args[key] = value
end
return { type = "group", name = GetBossTreeLabel(encounterId), args = args }
end
local function BuildRaidTreeArgs()
local args = {}
local raidNames, buckets = GetRaidBuckets()
for raidIndex, raidName in ipairs(raidNames) do
local raidKey = tostring(buckets[raidName].key or raidIndex)
args["raid_" .. raidKey] = {
type = "group",
order = 100 + raidIndex,
name = raidName,
childGroups = "tree",
args = {
description = {
type = "description",
order = 1,
width = "full",
name = string.format("|cffffd100%s|r: %s", L["OPT_RT_RAID_NAME"] or "Raid", raidName),
},
raidId = {
type = "description",
order = 2,
width = "full",
name = string.format("|cffffd100%s|r: %s", L["OPT_RT_RAID_ID"] or "Raid ID", tostring(buckets[raidName].key or raidIndex)),
},
},
}
local addedEncounterIds = {}
local bossOrder = 10
for _, difficultyKey in ipairs(DIFFICULTY_KEYS) do
local encounterIds = buckets[raidName].difficulties[difficultyKey] or {}
for _, encounterId in ipairs(encounterIds) do
if not addedEncounterIds[encounterId] then
local encounterGroup = BuildEncounterDetailGroup(encounterId)
if encounterGroup then
encounterGroup.order = bossOrder
args["raid_" .. raidKey].args["boss_" .. tostring(encounterId)] = encounterGroup
bossOrder = bossOrder + 1
addedEncounterIds[encounterId] = true
end
end
end
end
end
return args, #raidNames
end
function RT:GetOptionsGroup()
local drafts = GetDrafts()
local raidTreeArgs, raidCount = BuildRaidTreeArgs()
local group = {
type = "group",
name = L["OPT_RT_NAME"] or "Raid Timeline",
order = 4,
childGroups = "tree",
args = {
general = {
type = "group",
order = 1,
name = L["OPT_RT_SECTION_GENERAL"] or "General",
args = {
enabled = {
type = "toggle",
order = 1,
width = "full",
name = L["OPT_RT_ENABLED"] or "Enable Raid Timeline",
get = function() return RT:GetSettings().enabled == true end,
set = function(_, val)
RT:GetSettings().enabled = val and true or false
if val then RT:Enable() else RT:Disable() end
end,
},
leadTime = {
type = "range",
order = 2,
min = 1,
max = 15,
step = 1,
name = L["OPT_RT_LEAD_TIME"] or "Warning lead time",
get = function() return tonumber(RT:GetSettings().leadTime) or 5 end,
set = function(_, val) RT:GetSettings().leadTime = math.floor((tonumber(val) or 5) + 0.5) end,
},
assignmentLeadTime = {
type = "range",
order = 2.1,
min = 0,
max = 60,
step = 1,
name = L["OPT_RT_ASSIGNMENT_LEAD_TIME"] or "Assignment lead time",
desc = L["OPT_RT_ASSIGNMENT_LEAD_TIME_DESC"] or "How many seconds before the planned use the assigned player should be selected.",
get = function()
local settings = RT:GetSettings()
return tonumber(settings.assignmentLeadTime) or tonumber(settings.leadTime) or 5
end,
set = function(_, val)
RT:GetSettings().assignmentLeadTime = math.floor((tonumber(val) or 5) + 0.5)
end,
},
header = { type = "header", order = 3, name = L["OPT_RT_ALERT_HEADER"] or "Alert frame" },
unlocked = {
type = "toggle", order = 4, width = "double", name = L["OPT_RT_UNLOCK"] or "Unlock alert frame",
get = function() return RT:GetSettings().unlocked == true end,
set = function(_, val)
RT:GetSettings().unlocked = val and true or false
RT:ApplyAlertStyle()
if val then RT:ShowPreview() end
end,
},
preview = {
type = "toggle", order = 5, width = "double", name = L["OPT_RT_PREVIEW"] or "Preview alert",
get = function() return RT.previewAlertActive == true end,
set = function(_, val) if val then RT:ShowPreview() else RT:HideAlert() end end,
},
alertFont = {
type = "select", order = 6, width = 1.4, name = L["OPT_FONT"] or "Typeface",
dialogControl = "LSM30_Font",
values = AceGUIWidgetLSMlists and AceGUIWidgetLSMlists.font or (LSM and LSM:HashTable("font")) or {},
get = function() return RT:GetSettings().alertFont or "Friz Quadrata TT" end,
set = function(_, val) RT:GetSettings().alertFont = val; RT:ApplyAlertStyle() end,
},
alertFontSize = {
type = "range", order = 7, min = 10, max = 72, step = 1, width = 1.0, name = L["OPT_FONT_SIZE"] or "Font size",
get = function() return tonumber(RT:GetSettings().alertFontSize) or 30 end,
set = function(_, val) RT:GetSettings().alertFontSize = math.floor((tonumber(val) or 30) + 0.5); RT:ApplyAlertStyle() end,
},
alertFontOutline = {
type = "select", order = 8, width = 1.0, name = L["OPT_FONT_OUTLINE"] or "Font outline",
values = FONT_OUTLINE_VALUES,
get = function() return RT:GetSettings().alertFontOutline or "OUTLINE" end,
set = function(_, val) RT:GetSettings().alertFontOutline = val; RT:ApplyAlertStyle() end,
},
alertColor = {
type = "color", order = 9, width = 0.8, name = L["OPT_RT_ALERT_COLOR"] or "Text colour", hasAlpha = true,
get = function()
local color = RT:GetSettings().alertColor or {}
return color.r or 1, color.g or 0.82, color.b or 0.15, color.a or 1
end,
set = function(_, r, g, b, a)
local color = RT:GetSettings().alertColor
color.r, color.g, color.b, color.a = r, g, b, a
RT:ApplyAlertStyle()
end,
},
desc = {
type = "description", order = 10, width = "full",
name = L["OPT_RT_DESC"] or "Create encounter timelines here and open the interactive Ace3 timeline editor for visual planning.",
},
},
},
manage = {
type = "group",
order = 2,
name = L["OPT_RT_SECTION_MANAGE"] or "Manage encounters",
args = {
header = { type = "header", order = 1, name = L["OPT_RT_ENCOUNTERS_HEADER"] or "Encounter timelines" },
addEncounterId = {
type = "input", order = 2, width = 0.8, name = L["OPT_RT_ADD_ENCOUNTER_ID"] or "Encounter ID",
disabled = function() return not RT:IsLocalEditor() end,
get = function() return tostring(drafts.addEncounterId or "") end,
set = function(_, val) drafts.addEncounterId = val end,
},
addEncounterName = {
type = "input", order = 3, width = 1.2, name = L["OPT_RT_ADD_ENCOUNTER_NAME"] or "Encounter name",
disabled = function() return not RT:IsLocalEditor() end,
get = function() return tostring(drafts.addEncounterName or "") end,
set = function(_, val) drafts.addEncounterName = val end,
},
addEncounter = {
type = "execute", order = 5, width = "full", name = L["OPT_RT_ADD_ENCOUNTER"] or "Add encounter",
disabled = function() return not RT:IsLocalEditor() end,
func = function()
if RT:AddEncounter(drafts.addEncounterId, drafts.addEncounterName) then
drafts.addEncounterId, drafts.addEncounterName = "", ""
Notify(true)
else
HMGT:Print(L["OPT_RT_INVALID_ENCOUNTER"] or "HMGT: invalid encounter ID")
end
end,
},
summary = {
type = "description", order = 6, width = "full",
name = function()
if raidCount == 0 then return L["OPT_RT_EMPTY"] or "No encounter timelines configured yet." end
return string.format("|cffffd100%s|r: %d", L["OPT_RT_ENCOUNTERS_HEADER"] or "Encounter timelines", #RT:GetEncounterIds())
end,
},
},
},
},
}
if raidCount == 0 then
group.args.empty = {
type = "group",
order = 50,
name = L["OPT_RT_EMPTY"] or "No encounter timelines configured yet.",
args = {
description = { type = "description", order = 1, width = "full", name = L["OPT_RT_EMPTY"] or "No encounter timelines configured yet." },
},
}
else
for key, value in pairs(raidTreeArgs) do
group.args[key] = value
end
end
return group
end
HMGT_Config:RegisterOptionsProvider("raidTimeline", function()
raidTimelineOptionsGroup = raidTimelineOptionsGroup or RT:GetOptionsGroup()
ClearOptionCaches()
local fresh = RT:GetOptionsGroup()
raidTimelineOptionsGroup.name = fresh.name
raidTimelineOptionsGroup.order = fresh.order
raidTimelineOptionsGroup.childGroups = fresh.childGroups
raidTimelineOptionsGroup.args = fresh.args
return {
path = "raidTimeline",
order = 4,
group = raidTimelineOptionsGroup,
}
end)