Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions BossTimelines/Beloren.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
local _, NSI = ... -- Internal namespace

--------------------------------------------------------------------------------
-- BELO'REN (3182)
-- Phoenix fight with ~110s cycles, intermissions at Death Drop
--------------------------------------------------------------------------------

local abilities = {
{name = "Voidlight Convergence", spellID = 1242515, category = "damage", phase = 1, times = {1, 82, 192, 302, 412}, duration = 4},
{name = "Light Dive", spellID = 1241291, category = "movement", phase = 1, times = {20, 101, 211, 321, 431}, duration = 3},
{name = "Void Edict", spellID = 1261218, category = "soak", phase = 1, times = {21, 107, 137, 217, 247, 327, 357, 437, 467, 487}, duration = 3},
{name = "Light Edict", spellID = 1261217, category = "soak", phase = 1, times = {26, 102, 132, 152, 212, 242, 262, 322, 352, 372, 432, 462, 482}, duration = 3},
{name = "Holy Burn", spellID = 1244348, category = "damage", phase = 1, times = {27, 108, 126, 148, 218, 236, 258, 328, 346, 368, 438, 456, 478}, duration = 3},
{name = "Death Drop", spellID = 1246709, category = "intermission", phase = 1, times = {40, 46, 150, 156, 260, 266, 370, 376}, duration = 3},
{name = "Incubation of Flames", spellID = 1242792, category = "intermission", phase = 1, times = {47, 157, 267, 377}, duration = 8},
{name = "Voidlight Edict", spellID = 1241640, category = "soak", phase = 1, times = {72, 112, 142, 222, 252, 332, 362}, duration = 3},
{name = "Light Quill", spellID = 1241992, category = "damage", phase = 1, times = {122, 232, 342, 452, 472}, duration = 3},
{name = "Guardian's Edict", spellID = 1260826, category = "soak", phase = 1, times = {128, 237, 347, 458}, duration = 4},
{name = "Radiant Echoes", spellID = 1242981, category = "damage", phase = 1, times = {158, 268, 378, 442, 488}, duration = 4},
}

local phases = {
[1] = {start = 0},
}

NSI.BossTimelines[3182] = {
Mythic = {
duration = 540,
phases = phases,
abilities = abilities,
},
Heroic = {
duration = 540,
phases = phases,
abilities = abilities,
},
}
298 changes: 298 additions & 0 deletions BossTimelines/BossTimelines.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
local _, NSI = ... -- Internal namespace

--[[
Boss Timeline Data

Structure (nested by difficulty):
NSI.BossTimelines[encounterID] = {
Mythic = { ... }, -- Mythic difficulty timeline
Heroic = { ... }, -- Heroic difficulty timeline
}

Each difficulty contains:
{
duration = number, -- Total fight duration in seconds
phases = {
[phaseNum] = {
start = number, -- Default phase start time in seconds
name = string, -- (optional) Phase display name
color = {r, g, b}, -- (optional) RGB color for phase (0-1 range)
},
},
abilities = {
{
name = string, -- Ability name
spellID = number, -- WoW spell ID for icon lookup
category = string, -- Comma-separated keywords (see below)
phase = number, -- Phase number (1, 2, 3, etc.)
times = {number, ...}, -- Array of cast times (seconds from phase start)
duration = number, -- Ability duration in seconds (0 if instant)
},
},
}

Category Keywords (comma-separated, e.g. "raid damage, debuffs"):
- raid damage / damage: Raid-wide damage requiring healing cooldowns
- tankbuster / tank: Tank-specific mechanics (busters, swaps)
- frontal: Frontal cone attacks (often combined with tankbuster)
- movement: Positioning/movement mechanics
- soak / group soak: Soak mechanics requiring assignments
- debuffs: Debuff application mechanics
- healing absorb: Healing absorption effects
- knock: Knockback mechanics
- event: Special event or intermission
- intermission: Phase transition abilities
]]

-- Initialize the BossTimelines table
NSI.BossTimelines = NSI.BossTimelines or {}

-- Category colors for timeline display
-- Maps category keywords to colors (supports compound categories like "raid damage, debuffs")
NSI.BossTimelineColors = {
-- Damage categories (Red)
damage = {0.9, 0.3, 0.3},
["raid damage"] = {0.9, 0.3, 0.3},

-- Tank categories (Blue)
tank = {0.3, 0.5, 0.9},
tankbuster = {0.3, 0.5, 0.9},
frontal = {0.3, 0.5, 0.9},

-- Movement categories (Yellow/Orange)
movement = {0.9, 0.7, 0.2},
knock = {0.9, 0.7, 0.2},

-- Soak categories (Green)
soak = {0.5, 0.9, 0.5},
["group soak"] = {0.5, 0.9, 0.5},

-- Debuff/Healing categories (Pink/Magenta)
debuffs = {0.9, 0.5, 0.7},
["healing absorb"] = {0.9, 0.5, 0.7},

-- Event/Intermission categories (Purple)
intermission = {0.7, 0.4, 0.9},
event = {0.7, 0.4, 0.9},
}

-- Category sort priority (lower = higher priority)
NSI.BossTimelineCategoryOrder = {
-- Damage first
damage = 1,
["raid damage"] = 1,
-- Then soak
soak = 2,
["group soak"] = 2,
-- Then tank
tank = 3,
tankbuster = 3,
frontal = 3,
-- Then debuffs
debuffs = 4,
["healing absorb"] = 4,
-- Then movement
movement = 5,
knock = 5,
-- Then events/intermission
event = 6,
intermission = 6,
}

-- Parse a compound category string and return color and sort order
-- e.g., "raid damage, debuffs" -> color for "raid damage", order 1
function NSI:ParseCategoryForDisplay(categoryStr)
if not categoryStr or categoryStr == "" then
return {0.7, 0.7, 0.7}, 99, "unknown" -- default gray
end

local color = nil
local order = 99
local primaryCategory = "unknown"

-- Split by comma and check each keyword
for keyword in categoryStr:gmatch("([^,]+)") do
keyword = strtrim(keyword):lower()

-- Check for color match (use first match found)
if not color and self.BossTimelineColors[keyword] then
color = self.BossTimelineColors[keyword]
primaryCategory = keyword
end

-- Check for sort order (use lowest/highest priority found)
local keywordOrder = self.BossTimelineCategoryOrder[keyword]
if keywordOrder and keywordOrder < order then
order = keywordOrder
if not color then
primaryCategory = keyword
end
end
end

return color or {0.7, 0.7, 0.7}, order, primaryCategory
end

-- Encounter name lookup
NSI.BossTimelineNames = {
[3176] = "Imperator Averzian",
[3177] = "Vorasius",
[3178] = "Vaelgor & Ezzorak",
[3179] = "Fallen King Salhadaar",
[3180] = "Lightblinded Vanguard",
[3181] = "Crown of the Cosmos",
[3182] = "Belo'ren",
[3183] = "Midnight Falls",
[3306] = "Chimaerus",
}

--------------------------------------------------------------------------------
-- HELPER FUNCTIONS
--------------------------------------------------------------------------------

-- Difficulty ID to name mapping
NSI.DifficultyNames = {
[15] = "Heroic",
[16] = "Mythic",
}

-- Get current difficulty name, defaults to "Mythic" if unknown
function NSI:GetCurrentDifficultyName()
local _, _, difficultyID = GetInstanceInfo()
return self.DifficultyNames[difficultyID] or "Mythic"
end

-- Get the timeline data for a specific encounter and difficulty
-- Falls back to Mythic > Heroic > Normal if requested difficulty not available
function NSI:GetBossTimeline(encounterID, difficulty)
local bossData = self.BossTimelines[encounterID]
if not bossData then return nil end

-- If difficulty specified, try that first
if difficulty and bossData[difficulty] then
return bossData[difficulty], difficulty
end

-- Auto-detect current difficulty
local currentDiff = self:GetCurrentDifficultyName()
if bossData[currentDiff] then
return bossData[currentDiff], currentDiff
end

-- Fallback chain: Mythic > Heroic
if bossData.Mythic then return bossData.Mythic, "Mythic" end
if bossData.Heroic then return bossData.Heroic, "Heroic" end

return nil
end

-- Get user-adjusted phase start time, or default if not set
function NSI:GetPhaseStart(encounterID, phaseNum, difficulty)
-- Phase 1 always starts at 0
if phaseNum == 1 then return 0 end

local timeline = self:GetBossTimeline(encounterID, difficulty)
if not timeline or not timeline.phases or not timeline.phases[phaseNum] then return 0 end

-- Check for user adjustment
if NSRT.PhaseTimings and NSRT.PhaseTimings[encounterID] and NSRT.PhaseTimings[encounterID][phaseNum] then
return NSRT.PhaseTimings[encounterID][phaseNum]
end

return timeline.phases[phaseNum].start
end

-- Set user-adjusted phase start time
function NSI:SetPhaseStart(encounterID, phaseNum, time)
-- Cannot adjust phase 1
if phaseNum == 1 then return end

if not NSRT.PhaseTimings then
NSRT.PhaseTimings = {}
end
if not NSRT.PhaseTimings[encounterID] then
NSRT.PhaseTimings[encounterID] = {}
end

NSRT.PhaseTimings[encounterID][phaseNum] = time
end

-- Reset phase timing to default
function NSI:ResetPhaseStart(encounterID, phaseNum)
if NSRT.PhaseTimings and NSRT.PhaseTimings[encounterID] then
NSRT.PhaseTimings[encounterID][phaseNum] = nil
end
end

-- Get all abilities for an encounter with absolute times
function NSI:GetBossTimelineAbilities(encounterID, difficulty)
local timeline, actualDifficulty = self:GetBossTimeline(encounterID, difficulty)
if not timeline then return nil end

-- Pre-calculate all phase start times for filtering
local phaseStarts = {}
local maxPhase = 0
for phaseNum, _ in pairs(timeline.phases) do
phaseStarts[phaseNum] = self:GetPhaseStart(encounterID, phaseNum, actualDifficulty)
if phaseNum > maxPhase then
maxPhase = phaseNum
end
end

local result = {}

for i, ability in ipairs(timeline.abilities) do
local phaseStart = self:GetPhaseStart(encounterID, ability.phase, actualDifficulty)
local absoluteTimes = {}

-- Get the start time of the next phase (if it exists)
-- Abilities from this phase should not extend past the next phase start
local nextPhaseStart = nil
if ability.phase < maxPhase then
nextPhaseStart = phaseStarts[ability.phase + 1]
end

for _, time in ipairs(ability.times) do
local absoluteTime = phaseStart + time
-- Filter out abilities that occur after the next phase has started
if not nextPhaseStart or absoluteTime < nextPhaseStart then
table.insert(absoluteTimes, absoluteTime)
end
end

-- Only add ability if it has any visible times
if #absoluteTimes > 0 then
-- Parse compound category for color and sort order
local color, sortOrder, primaryCategory = self:ParseCategoryForDisplay(ability.category)

table.insert(result, {
name = ability.name,
spellID = ability.spellID,
category = ability.category, -- Keep original for tooltip display
primaryCategory = primaryCategory, -- Parsed primary category
sortOrder = sortOrder, -- For sorting in timeline
phase = ability.phase,
times = absoluteTimes,
duration = ability.duration,
color = color,
})
end
end

-- Build phases with adjusted times
local phases = {}
for phaseNum, phaseData in pairs(timeline.phases) do
phases[phaseNum] = {
name = phaseData.name, -- May be nil
start = self:GetPhaseStart(encounterID, phaseNum, actualDifficulty),
color = phaseData.color, -- May be nil
}
end

return result, timeline.duration, phases, actualDifficulty
end

-- Get encounter name from ID
function NSI:GetEncounterName(encounterID)
return self.BossTimelineNames[encounterID] or ("Encounter " .. encounterID)
end
13 changes: 13 additions & 0 deletions BossTimelines/BossTimelines.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ https://raw.githubusercontent.com/Gethe/wow-ui-source/live/Interface/FrameXML/UI.xsd">
<!-- Core initialization and helper functions (must load first) -->
<Script file="BossTimelines.lua"/>

<!-- Individual boss timelines -->
<Script file="ImperatorAverzian.lua"/>
<Script file="Vorasius.lua"/>
<Script file="VaelgorEzzorak.lua"/>
<Script file="FallenKingSalhadaar.lua"/>
<Script file="LightblindedVanguard.lua"/>
<Script file="Beloren.lua"/>
<Script file="Chimaerus.lua"/>
</Ui>
Loading