diff --git a/Definitions.lua b/Definitions.lua
index 4eda17e4c..35e685312 100644
--- a/Definitions.lua
+++ b/Definitions.lua
@@ -181,8 +181,11 @@
---@field ListInstances fun(self: details) : instance[] return a table with all the instances
---@field UnpackMythicDungeonInfo fun(self: details, mythicDungeonInfo: mythicdungeoninfo) : boolean, segmentid, number, number, number, string, number, string, number, number, number unpack the mythic dungeon info and return the values
---@field CreateRightClickToCloseLabel fun(self: details, parent: frame) : df_label return a df_label with the text "Right click to close", need to set point
+---@field IsValidActor fun(self: details, actor: actor) : boolean return true if the actor is valid
---@field
---@field
+---@field
+
---@class detailseventlistener : table
diff --git a/Details.toc b/Details.toc
index 7953cdcdc..29be5f2c2 100644
--- a/Details.toc
+++ b/Details.toc
@@ -77,6 +77,7 @@ functions\textures.lua
functions\journal.lua
functions\commentator.lua
functions\chat_embed.lua
+functions\storage.lua
core\aura_scan.lua
core\timemachine.lua
diff --git a/Details_Cata.toc b/Details_Cata.toc
index b1c84f398..7077702e4 100644
--- a/Details_Cata.toc
+++ b/Details_Cata.toc
@@ -76,6 +76,7 @@ functions\textures.lua
functions\journal.lua
functions\commentator.lua
functions\chat_embed.lua
+functions\storage.lua
core\aura_scan.lua
core\timemachine.lua
diff --git a/Details_Classic.toc b/Details_Classic.toc
index e710c879c..94fd4e584 100644
--- a/Details_Classic.toc
+++ b/Details_Classic.toc
@@ -71,6 +71,7 @@ functions\warcraftlogs.lua
functions\textures.lua
functions\commentator.lua
functions\chat_embed.lua
+functions\storage.lua
core\aura_scan.lua
core\timemachine.lua
diff --git a/Details_Wrath.toc b/Details_Wrath.toc
index 7780232cc..ce9812b3f 100644
--- a/Details_Wrath.toc
+++ b/Details_Wrath.toc
@@ -71,6 +71,7 @@ functions\warcraftlogs.lua
functions\textures.lua
functions\commentator.lua
functions\chat_embed.lua
+functions\storage.lua
core\aura_scan.lua
core\timemachine.lua
diff --git a/Libs/DF/load.xml b/Libs/DF/load.xml
index 797e8d789..accfddc8b 100644
--- a/Libs/DF/load.xml
+++ b/Libs/DF/load.xml
@@ -34,7 +34,8 @@
-
+
+
diff --git a/Libs/DF/panel.lua b/Libs/DF/panel.lua
index b91b8539a..f82bafa8d 100644
--- a/Libs/DF/panel.lua
+++ b/Libs/DF/panel.lua
@@ -4890,688 +4890,6 @@ function detailsFramework:CreateBorderFrame(parent, name)
return f
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
---horizontal scroll frame
-
-local timeline_options = {
- width = 400,
- height = 700,
- line_height = 20,
- line_padding = 1,
-
- show_elapsed_timeline = true,
- elapsed_timeline_height = 20,
-
- --space to put the player/spell name and icons
- header_width = 150,
-
- --how many pixels will be use to represent 1 second
- pixels_per_second = 20,
-
- scale_min = 0.15,
- scale_max = 1,
-
- backdrop = {edgeFile = [[Interface\Buttons\WHITE8X8]], edgeSize = 1, bgFile = [[Interface\Tooltips\UI-Tooltip-Background]], tileSize = 64, tile = true},
- backdrop_color = {0, 0, 0, 0.2},
- backdrop_color_highlight = {.2, .2, .2, 0.4},
- backdrop_border_color = {0.1, 0.1, 0.1, .2},
-
- slider_backdrop = {edgeFile = [[Interface\Buttons\WHITE8X8]], edgeSize = 1, bgFile = [[Interface\Tooltips\UI-Tooltip-Background]], tileSize = 64, tile = true},
- slider_backdrop_color = {0, 0, 0, 0.2},
- slider_backdrop_border_color = {0.1, 0.1, 0.1, .2},
-
- title_template = "ORANGE_FONT_TEMPLATE",
- text_tempate = "OPTIONS_FONT_TEMPLATE",
-
- on_enter = function(self)
- self:SetBackdropColor(unpack(self.backdrop_color_highlight))
- end,
- on_leave = function(self)
- self:SetBackdropColor(unpack(self.backdrop_color))
- end,
-
- block_on_enter = function(self)
-
- end,
- block_on_leave = function(self)
-
- end,
-}
-
-local elapsedtime_frame_options = {
- backdrop = {bgFile = [[Interface\Tooltips\UI-Tooltip-Background]], tileSize = 64, tile = true},
- backdrop_color = {.3, .3, .3, .7},
-
- text_color = {1, 1, 1, 1},
- text_size = 12,
- text_font = "Arial Narrow",
- text_outline = "NONE",
-
- height = 20,
-
- distance = 200, --distance in pixels between each label informing the time
- distance_min = 50, --minimum distance in pixels
- draw_line = true, --if true it'll draw a vertical line to represent a segment
- draw_line_color = {1, 1, 1, 0.2},
- draw_line_thickness = 1,
-}
-
-detailsFramework.TimeLineElapsedTimeFunctions = {
- --get a label and update its appearance
- GetLabel = function(self, index)
- local label = self.labels [index]
-
- if (not label) then
- label = self:CreateFontString(nil, "artwork", "GameFontNormal")
- label.line = self:CreateTexture(nil, "artwork")
- label.line:SetColorTexture(1, 1, 1)
- label.line:SetPoint("topleft", label, "bottomleft", 0, -2)
- self.labels [index] = label
- end
-
- detailsFramework:SetFontColor(label, self.options.text_color)
- detailsFramework:SetFontSize(label, self.options.text_size)
- detailsFramework:SetFontFace (label, self.options.text_font)
- detailsFramework:SetFontOutline (label, self.options.text_outline)
-
- if (self.options.draw_line) then
- label.line:SetVertexColor(unpack(self.options.draw_line_color))
- label.line:SetWidth(self.options.draw_line_thickness)
- label.line:Show()
- else
- label.line:Hide()
- end
-
- return label
- end,
-
- Reset = function(self)
- for i = 1, #self.labels do
- self.labels [i]:Hide()
- end
- end,
-
- Refresh = function(self, elapsedTime, scale)
- local parent = self:GetParent()
-
- self:SetHeight(self.options.height)
- local effectiveArea = self:GetWidth() --already scaled down width
- local pixelPerSecond = elapsedTime / effectiveArea --how much 1 pixels correlate to time
-
- local distance = self.options.distance --pixels between each segment
- local minDistance = self.options.distance_min --min pixels between each segment
-
- --scale the distance between each label showing the time with the parent's scale
- distance = distance * scale
- distance = max(distance, minDistance)
-
- local amountSegments = ceil (effectiveArea / distance)
-
- for i = 1, amountSegments do
- local label = self:GetLabel (i)
- local xOffset = distance * (i - 1)
- label:SetPoint("left", self, "left", xOffset, 0)
-
- local secondsOfTime = pixelPerSecond * xOffset
-
- label:SetText(detailsFramework:IntegerToTimer(floor(secondsOfTime)))
-
- if (label.line:IsShown()) then
- label.line:SetHeight(parent:GetParent():GetHeight())
- end
-
- label:Show()
- end
- end,
-}
-
---creates a frame to show the elapsed time in a row
-function detailsFramework:CreateElapsedTimeFrame(parent, name, options)
- local elapsedTimeFrame = CreateFrame("frame", name, parent, "BackdropTemplate")
-
- detailsFramework:Mixin(elapsedTimeFrame, detailsFramework.OptionsFunctions)
- detailsFramework:Mixin(elapsedTimeFrame, detailsFramework.LayoutFrame)
- detailsFramework:Mixin(elapsedTimeFrame, detailsFramework.TimeLineElapsedTimeFunctions)
-
- elapsedTimeFrame:BuildOptionsTable(elapsedtime_frame_options, options)
-
- elapsedTimeFrame:SetBackdrop(elapsedTimeFrame.options.backdrop)
- elapsedTimeFrame:SetBackdropColor(unpack(elapsedTimeFrame.options.backdrop_color))
-
- elapsedTimeFrame.labels = {}
-
- return elapsedTimeFrame
-end
-
-
-detailsFramework.TimeLineBlockFunctions = {
- --self is the line
- SetBlock = function(self, index, blockInfo)
- --get the block information
- --see what is the current scale
- --adjust the block position
-
- local block = self:GetBlock (index)
-
- --need:
- --the total time of the timeline
- --the current scale of the timeline
- --the elapsed time of this block
- --icon of the block
- --text
- --background color
-
- end,
-
- SetBlocksFromData = function(self)
- local parent = self:GetParent():GetParent()
- local data = parent.data
- local defaultColor = parent.defaultColor --guarantee to have a value
-
- self:Show()
-
- --none of these values are scaled, need to calculate
- local pixelPerSecond = parent.pixelPerSecond
- local totalLength = parent.totalLength
- local scale = parent.currentScale
-
- pixelPerSecond = pixelPerSecond * scale
-
- local headerWidth = parent.headerWidth
-
- --dataIndex stores which line index from the data this line will use
- --lineData store members: .text .icon .timeline
- local lineData = data.lines[self.dataIndex]
-
- self.spellId = lineData.spellId
-
- --if there's an icon, anchor the text at the right side of the icon
- --this is the title and icon of the title
- if (lineData.icon) then
- self.icon:SetTexture(lineData.icon)
- if (lineData.coords) then
- self.icon:SetTexCoord(unpack(lineData.coords))
- else
- self.icon:SetTexCoord(.1, .9, .1, .9)
- end
- self.text:SetText(lineData.text or "")
- self.text:SetPoint("left", self.icon.widget, "right", 2, 0)
- else
- self.icon:SetTexture(nil)
- self.text:SetText(lineData.text or "")
- self.text:SetPoint("left", self, "left", 2, 0)
- end
-
- if (self.dataIndex % 2 == 1) then
- self:SetBackdropColor(0, 0, 0, 0)
- else
- local r, g, b, a = unpack(self.backdrop_color)
- self:SetBackdropColor(r, g, b, a)
- end
-
- self:SetWidth(5000)
-
- local timelineData = lineData.timeline
- local spellId = lineData.spellId
- local useIconOnBlock = data.useIconOnBlocks
-
- local baseFrameLevel = parent:GetFrameLevel() + 10
-
- for i = 1, #timelineData do
- local blockInfo = timelineData[i]
-
- local timeInSeconds = blockInfo[1]
- local length = blockInfo[2]
- local isAura = blockInfo[3]
- local auraDuration = blockInfo[4]
- local blockSpellId = blockInfo[5]
-
- local payload = blockInfo.payload
-
- local xOffset = pixelPerSecond * timeInSeconds
- local width = pixelPerSecond * length
-
- if (timeInSeconds < -0.2) then
- xOffset = xOffset / 2.5
- end
-
- local block = self:GetBlock(i)
- block:Show()
- block:SetFrameLevel(baseFrameLevel + i)
-
- PixelUtil.SetPoint(block, "left", self, "left", xOffset + headerWidth, 0)
-
- block.info.spellId = blockSpellId or spellId
- block.info.time = timeInSeconds
- block.info.duration = auraDuration
- block.info.payload = payload
-
- if (useIconOnBlock) then
- local iconTexture = lineData.icon
- if (blockSpellId) then
- iconTexture = GetSpellTexture(blockSpellId)
- end
-
- block.icon:SetTexture(iconTexture)
- block.icon:SetTexCoord(.1, .9, .1, .9)
- block.icon:SetAlpha(.834)
- block.icon:SetSize(self:GetHeight(), self:GetHeight())
-
- if (timeInSeconds < -0.2) then
- block.icon:SetDesaturated(true)
- else
- block.icon:SetDesaturated(false)
- end
-
- PixelUtil.SetSize(block, self:GetHeight(), self:GetHeight())
-
- if (isAura) then
- block.auraLength:Show()
- block.auraLength:SetWidth(pixelPerSecond * isAura)
- block:SetWidth(max(pixelPerSecond * isAura, 16))
- else
- block.auraLength:Hide()
- end
-
- block.background:SetVertexColor(0, 0, 0, 0)
- else
- block.background:SetVertexColor(0, 0, 0, 0)
- PixelUtil.SetSize(block, max(width, 16), self:GetHeight())
- block.auraLength:Hide()
- end
- end
- end,
-
- GetBlock = function(self, index)
- local block = self.blocks [index]
- if (not block) then
- block = CreateFrame("frame", nil, self, "BackdropTemplate")
- self.blocks [index] = block
-
- local background = block:CreateTexture(nil, "background")
- background:SetColorTexture(1, 1, 1, 1)
- local icon = block:CreateTexture(nil, "artwork")
- local text = block:CreateFontString(nil, "artwork")
- local auraLength = block:CreateTexture(nil, "border")
-
- background:SetAllPoints()
- icon:SetPoint("left")
- text:SetPoint("left", icon, "left", 2, 0)
- auraLength:SetPoint("topleft", icon, "topleft", 0, 0)
- auraLength:SetPoint("bottomleft", icon, "bottomleft", 0, 0)
- auraLength:SetColorTexture(1, 1, 1, 1)
- auraLength:SetVertexColor(1, 1, 1, 0.1)
-
- block.icon = icon
- block.text = text
- block.background = background
- block.auraLength = auraLength
-
- block:SetScript("OnEnter", self:GetParent():GetParent().options.block_on_enter)
- block:SetScript("OnLeave", self:GetParent():GetParent().options.block_on_leave)
-
- block:SetMouseClickEnabled(false)
- block.info = {}
- end
-
- return block
- end,
-
- Reset = function(self)
- --attention, it doesn't reset icon texture, text and background color
- for i = 1, #self.blocks do
- self.blocks [i]:Hide()
- end
- self:Hide()
- end,
-}
-
-detailsFramework.TimeLineFunctions = {
- GetLine = function(self, index)
- local line = self.lines [index]
- if (not line) then
- --create a new line
- line = CreateFrame("frame", "$parentLine" .. index, self.body, "BackdropTemplate")
- detailsFramework:Mixin(line, detailsFramework.TimeLineBlockFunctions)
- self.lines [index] = line
-
- local lineHeader = CreateFrame("frame", nil, line, "BackdropTemplate")
- lineHeader:SetPoint("topleft", line, "topleft", 0, 0)
- lineHeader:SetPoint("bottomleft", line, "bottomleft", 0, 0)
- lineHeader:SetScript("OnEnter", self.options.header_on_enter)
- lineHeader:SetScript("OnLeave", self.options.header_on_leave)
-
- line.lineHeader = lineHeader
-
- --store the individual textures that shows the timeline information
- line.blocks = {}
- line.SetBlock = detailsFramework.TimeLineBlockFunctions.SetBlock
- line.GetBlock = detailsFramework.TimeLineBlockFunctions.GetBlock
-
- --set its parameters
-
- if (self.options.show_elapsed_timeline) then
- line:SetPoint("topleft", self.body, "topleft", 1, -((index-1) * (self.options.line_height + 1)) - 2 - self.options.elapsed_timeline_height)
- else
- line:SetPoint("topleft", self.body, "topleft", 1, -((index-1) * (self.options.line_height + 1)) - 1)
- end
- line:SetSize(1, self.options.line_height) --width is set when updating the frame
-
- line:SetScript("OnEnter", self.options.on_enter)
- line:SetScript("OnLeave", self.options.on_leave)
- line:SetMouseClickEnabled(false)
-
- line:SetBackdrop(self.options.backdrop)
- line:SetBackdropColor(unpack(self.options.backdrop_color))
- line:SetBackdropBorderColor(unpack(self.options.backdrop_border_color))
-
- local icon = detailsFramework:CreateImage(line, "", self.options.line_height, self.options.line_height)
- icon:SetPoint("left", line, "left", 2, 0)
- line.icon = icon
-
- local text = detailsFramework:CreateLabel(line, "", detailsFramework:GetTemplate("font", self.options.title_template))
- text:SetPoint("left", icon.widget, "right", 2, 0)
- line.text = text
-
- line.backdrop_color = self.options.backdrop_color or {.1, .1, .1, .3}
- line.backdrop_color_highlight = self.options.backdrop_color_highlight or {.3, .3, .3, .5}
- end
-
- return line
- end,
-
- ResetAllLines = function(self)
- for i = 1, #self.lines do
- self.lines[i]:Reset()
- end
- end,
-
- AdjustScale = function(self, index)
-
- end,
-
- --todo
- --make the on enter and leave tooltips
- --set icons and texts
- --skin the sliders
-
- RefreshTimeLine = function(self)
- --debug
- --self.currentScale = 1
-
- --calculate the total width
- local pixelPerSecond = self.options.pixels_per_second
- local totalLength = self.data.length or 1
- local currentScale = self.currentScale
-
- self.scaleSlider:Enable()
-
- --how many pixels represent 1 second
- local bodyWidth = totalLength * pixelPerSecond * currentScale
- self.body:SetWidth(bodyWidth + self.options.header_width)
- self.body.effectiveWidth = bodyWidth
-
- --reduce the default canvas size from the body with and don't allow the max value be negative
- local newMaxValue = max(bodyWidth - (self:GetWidth() - self.options.header_width), 0)
-
- --adjust the scale slider range
- local oldMin, oldMax = self.horizontalSlider:GetMinMaxValues()
- self.horizontalSlider:SetMinMaxValues(0, newMaxValue)
- self.horizontalSlider:SetValue(detailsFramework:MapRangeClamped(oldMin, oldMax, 0, newMaxValue, self.horizontalSlider:GetValue()))
-
- local defaultColor = self.data.defaultColor or {1, 1, 1, 1}
-
- --cache values
- self.pixelPerSecond = pixelPerSecond
- self.totalLength = totalLength
- self.defaultColor = defaultColor
- self.headerWidth = self.options.header_width
-
- --calculate the total height
- local lineHeight = self.options.line_height
- local linePadding = self.options.line_padding
-
- local bodyHeight = (lineHeight + linePadding) * #self.data.lines
- self.body:SetHeight(bodyHeight)
- self.verticalSlider:SetMinMaxValues(0, max(bodyHeight - self:GetHeight(), 0))
- self.verticalSlider:SetValue(0)
-
- --refresh lines
- self:ResetAllLines()
- for i = 1, #self.data.lines do
- local line = self:GetLine(i)
- line.dataIndex = i --this index is used inside the line update function to know which data to get
- line.lineHeader:SetWidth(self.options.header_width)
- line:SetBlocksFromData() --the function to update runs within the line object
- end
-
- --refresh elapsed time frame
- --the elapsed frame must have a width before the refresh function is called
- self.elapsedTimeFrame:ClearAllPoints()
- self.elapsedTimeFrame:SetPoint("topleft", self.body, "topleft", self.options.header_width, 0)
- self.elapsedTimeFrame:SetPoint("topright", self.body, "topright", 0, 0)
- self.elapsedTimeFrame:Reset()
-
- self.elapsedTimeFrame:Refresh(self.data.length, self.currentScale)
- end,
-
- SetData = function(self, data)
- self.data = data
- self:RefreshTimeLine()
- end,
-
- GetData = function(self)
- return self.data
- end,
-}
-
---creates a regular scroll in horizontal position
-function detailsFramework:CreateTimeLineFrame(parent, name, options, timelineOptions)
- local width = options and options.width or timeline_options.width
- local height = options and options.height or timeline_options.height
- local scrollWidth = 800 --placeholder until the timeline receives data
- local scrollHeight = 800 --placeholder until the timeline receives data
-
- local frameCanvas = CreateFrame("scrollframe", name, parent, "BackdropTemplate")
-
- detailsFramework:Mixin(frameCanvas, detailsFramework.TimeLineFunctions)
- detailsFramework:Mixin(frameCanvas, detailsFramework.OptionsFunctions)
- detailsFramework:Mixin(frameCanvas, detailsFramework.LayoutFrame)
-
- frameCanvas.data = {}
- frameCanvas.lines = {}
- frameCanvas.currentScale = 0.5
- frameCanvas:SetSize(width, height)
-
- detailsFramework:ApplyStandardBackdrop(frameCanvas)
-
- local frameBody = CreateFrame("frame", nil, frameCanvas, "BackdropTemplate")
- frameBody:SetSize(scrollWidth, scrollHeight)
-
- frameCanvas:SetScrollChild(frameBody)
- frameCanvas.body = frameBody
-
- frameCanvas:BuildOptionsTable(timeline_options, options)
-
- --create elapsed time frame
- frameCanvas.elapsedTimeFrame = detailsFramework:CreateElapsedTimeFrame(frameBody, frameCanvas:GetName() and frameCanvas:GetName() .. "ElapsedTimeFrame", timelineOptions)
-
- local thumbColor = 0.95
- local scrollBackgroudColor = {0.05, 0.05, 0.05, 0.7}
-
- --create horizontal slider
- local horizontalSlider = CreateFrame("slider", frameCanvas:GetName() .. "HorizontalSlider", parent, "BackdropTemplate")
- horizontalSlider.bg = horizontalSlider:CreateTexture(nil, "background")
- horizontalSlider.bg:SetAllPoints(true)
- horizontalSlider.bg:SetColorTexture(unpack(scrollBackgroudColor))
- frameCanvas.horizontalSlider = horizontalSlider
-
- horizontalSlider:SetBackdrop(frameCanvas.options.slider_backdrop)
- horizontalSlider:SetBackdropColor(unpack(frameCanvas.options.slider_backdrop_color))
- horizontalSlider:SetBackdropBorderColor(unpack(frameCanvas.options.slider_backdrop_border_color))
-
- horizontalSlider.thumb = horizontalSlider:CreateTexture(nil, "OVERLAY")
- horizontalSlider.thumb:SetTexture("Interface\\Buttons\\UI-ScrollBar-Knob")
- horizontalSlider.thumb:SetSize(24, 24)
- horizontalSlider.thumb:SetVertexColor(thumbColor, thumbColor, thumbColor, 0.95)
- horizontalSlider:SetThumbTexture(horizontalSlider.thumb)
-
- horizontalSlider:SetOrientation("horizontal")
- horizontalSlider:SetSize(width + 20, 20)
- horizontalSlider:SetPoint("topleft", frameCanvas, "bottomleft")
- horizontalSlider:SetMinMaxValues(0, scrollWidth)
- horizontalSlider:SetValue(0)
- horizontalSlider:SetScript("OnValueChanged", function(self)
- local _, maxValue = horizontalSlider:GetMinMaxValues()
- local stepValue = ceil(ceil(self:GetValue() * maxValue) / max(maxValue, SMALL_FLOAT))
- if (stepValue ~= horizontalSlider.currentValue) then
- horizontalSlider.currentValue = stepValue
- frameCanvas:SetHorizontalScroll(stepValue)
- end
- end)
-
- --create scale slider
- local scaleSlider = CreateFrame("slider", frameCanvas:GetName() .. "ScaleSlider", parent, "BackdropTemplate")
- scaleSlider.bg = scaleSlider:CreateTexture(nil, "background")
- scaleSlider.bg:SetAllPoints(true)
- scaleSlider.bg:SetColorTexture(unpack(scrollBackgroudColor))
- scaleSlider:Disable()
- frameCanvas.scaleSlider = scaleSlider
-
- scaleSlider:SetBackdrop(frameCanvas.options.slider_backdrop)
- scaleSlider:SetBackdropColor(unpack(frameCanvas.options.slider_backdrop_color))
- scaleSlider:SetBackdropBorderColor(unpack(frameCanvas.options.slider_backdrop_border_color))
-
- scaleSlider.thumb = scaleSlider:CreateTexture(nil, "OVERLAY")
- scaleSlider.thumb:SetTexture("Interface\\Buttons\\UI-ScrollBar-Knob")
- scaleSlider.thumb:SetSize(24, 24)
- scaleSlider.thumb:SetVertexColor(thumbColor, thumbColor, thumbColor, 0.95)
- scaleSlider:SetThumbTexture(scaleSlider.thumb)
-
- scaleSlider:SetOrientation("horizontal")
- scaleSlider:SetSize(width + 20, 20)
- scaleSlider:SetPoint("topleft", horizontalSlider, "bottomleft", 0, -2)
- scaleSlider:SetMinMaxValues(frameCanvas.options.scale_min, frameCanvas.options.scale_max)
- scaleSlider:SetValue(detailsFramework:GetRangeValue(frameCanvas.options.scale_min, frameCanvas.options.scale_max, 0.5))
-
- scaleSlider:SetScript("OnValueChanged", function(self)
- local stepValue = ceil(self:GetValue() * 100) / 100
- if (stepValue ~= frameCanvas.currentScale) then
- local current = stepValue
- frameCanvas.currentScale = stepValue
- frameCanvas:RefreshTimeLine()
- end
- end)
-
- --create vertical slider
- local verticalSlider = CreateFrame("slider", frameCanvas:GetName() .. "VerticalSlider", parent, "BackdropTemplate")
- verticalSlider.bg = verticalSlider:CreateTexture(nil, "background")
- verticalSlider.bg:SetAllPoints(true)
- verticalSlider.bg:SetColorTexture(unpack(scrollBackgroudColor))
- frameCanvas.verticalSlider = verticalSlider
-
- verticalSlider:SetBackdrop(frameCanvas.options.slider_backdrop)
- verticalSlider:SetBackdropColor(unpack(frameCanvas.options.slider_backdrop_color))
- verticalSlider:SetBackdropBorderColor(unpack(frameCanvas.options.slider_backdrop_border_color))
-
- verticalSlider.thumb = verticalSlider:CreateTexture(nil, "OVERLAY")
- verticalSlider.thumb:SetTexture("Interface\\Buttons\\UI-ScrollBar-Knob")
- verticalSlider.thumb:SetSize(24, 24)
- verticalSlider.thumb:SetVertexColor(thumbColor, thumbColor, thumbColor, 0.95)
- verticalSlider:SetThumbTexture(verticalSlider.thumb)
-
- verticalSlider:SetOrientation("vertical")
- verticalSlider:SetSize(20, height - 2)
- verticalSlider:SetPoint("topleft", frameCanvas, "topright", 0, 0)
- verticalSlider:SetMinMaxValues(0, scrollHeight)
- verticalSlider:SetValue(0)
- verticalSlider:SetScript("OnValueChanged", function(self)
- frameCanvas:SetVerticalScroll(self:GetValue())
- end)
-
- --mouse scroll
- frameCanvas:EnableMouseWheel(true)
- frameCanvas:SetScript("OnMouseWheel", function(self, delta)
- local minValue, maxValue = horizontalSlider:GetMinMaxValues()
- local currentHorizontal = horizontalSlider:GetValue()
-
- if (IsShiftKeyDown() and delta < 0) then
- local amountToScroll = frameBody:GetHeight() / 20
- verticalSlider:SetValue(verticalSlider:GetValue() + amountToScroll)
-
- elseif (IsShiftKeyDown() and delta > 0) then
- local amountToScroll = frameBody:GetHeight() / 20
- verticalSlider:SetValue(verticalSlider:GetValue() - amountToScroll)
-
- elseif (IsControlKeyDown() and delta > 0) then
- scaleSlider:SetValue(min(scaleSlider:GetValue() + 0.1, 1))
-
- elseif (IsControlKeyDown() and delta < 0) then
- scaleSlider:SetValue(max(scaleSlider:GetValue() - 0.1, 0.15))
-
- elseif (delta < 0 and currentHorizontal < maxValue) then
- local amountToScroll = frameBody:GetWidth() / 20
- horizontalSlider:SetValue(currentHorizontal + amountToScroll)
-
- elseif (delta > 0 and maxValue > 1) then
- local amountToScroll = frameBody:GetWidth() / 20
- horizontalSlider:SetValue(currentHorizontal - amountToScroll)
-
- end
- end)
-
- --mouse drag
- frameBody:SetScript("OnMouseDown", function(self, button)
- local x = GetCursorPosition()
- self.MouseX = x
-
- frameBody:SetScript("OnUpdate", function(self, deltaTime)
- local x = GetCursorPosition()
- local deltaX = self.MouseX - x
- local current = horizontalSlider:GetValue()
- horizontalSlider:SetValue(current +(deltaX * 1.2) *((IsShiftKeyDown() and 2) or(IsAltKeyDown() and 0.5) or 1))
- self.MouseX = x
- end)
- end)
-
- frameBody:SetScript("OnMouseUp", function(self, button)
- frameBody:SetScript("OnUpdate", nil)
- end)
-
- return frameCanvas
-end
-
-
---[=[
-local f = CreateFrame("frame", "TestFrame", UIParent)
-f:SetPoint("center")
-f:SetSize(900, 420)
-f:SetBackdrop({bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", tile = true, tileSize = 16, insets = {left = 1, right = 1, top = 0, bottom = 1}})
-
-local scroll = DF:CreateTimeLineFrame (f, "$parentTimeLine", {width = 880, height = 400})
-scroll:SetPoint("topleft", f, "topleft", 0, 0)
-
---need fake data to test fills
-scroll:SetData ({
- length = 360,
- defaultColor = {1, 1, 1, 1},
- lines = {
- {text = "player 1", icon = "", timeline = {
- --each table here is a block shown in the line
- --is an indexed table with: [1] time [2] length [3] color (if false, use the default) [4] text [5] icon [6] tooltip: if number = spellID tooltip, if table is text lines
- {1, 10}, {13, 11}, {25, 7}, {36, 5}, {55, 18}, {76, 30}, {105, 20}, {130, 11}, {155, 11}, {169, 7}, {199, 16}, {220, 18}, {260, 10}, {290, 23}, {310, 30}, {350, 10}
- }
- }, --end of line 1
- },
-})
-
-
-f:Hide()
-
---scroll.body:SetScale(0.5)
-
---]=]
-
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--error message box
diff --git a/Libs/DF/timeline.exemples.lua b/Libs/DF/timeline.exemples.lua
new file mode 100644
index 000000000..a07aaadd9
--- /dev/null
+++ b/Libs/DF/timeline.exemples.lua
@@ -0,0 +1,43 @@
+
+
+local DF = DetailsFramework
+
+local timelineFrame = TestTLFrame or CreateFrame("frame", "TestTLFrame", UIParent, "BackdropTemplate")
+timelineFrame:SetPoint("center")
+timelineFrame:SetSize(900, 420)
+timelineFrame:SetBackdrop({bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", tile = true, tileSize = 16, insets = {left = 1, right = 1, top = 0, bottom = 1}})
+
+local scroll = DF:CreateTimeLineFrame(timelineFrame, "$parentTimeLine", {width = 880, height = 400})
+scroll:SetPoint("topleft", timelineFrame, "topleft", 0, 0)
+
+--set data to test
+scroll:SetData({
+ length = 360,
+ defaultColor = {1, 1, 1, 1},
+ useIconOnBlocks = true,
+ lines = {
+ {
+ spellId = 17,
+ text = "player 1",
+ icon = [[Interface\ICONS\10Prof_PortableTable_Engineering01]],
+ timeline = {
+ ---[1] number timeInSeconds
+ ---[2] number length
+ ---[3] boolean isAura
+ ---[4] number auraDuration
+ ---[5] number blockSpellId
+ {1, 10}, {13, 11}, {25, 7}, {36, 5}, {55, 18}, {76, 30}, {105, 20}, {130, 11}, {155, 11}, {169, 7}, {199, 16}, {220, 18}, {260, 10}, {290, 23}, {310, 30}, {350, 10}
+ }
+ }, --end of line 1
+ {
+ spellId = 116,
+ text = "player 2",
+ icon = [[Interface\ICONS\10Prof_Table_Alchemy01]],
+ timeline = {
+ --each table here is a block shown in the line
+ --is an indexed table with: [1] time [2] length [3] color (if false, use the default) [4] text [5] icon [6] tooltip: if number = spellID tooltip, if table is text lines
+ {5, 10}, {20, 11}, {35, 7}, {40, 5}, {55, 18}, {70, 30}, {80, 20}, {90, 11}, {145, 11}, {180, 7}, {201, 16}, {223, 18}, {250, 10}, {280, 23}, {312, 30}, {330, 10}
+ }
+ }, --end of line 2
+ },
+})
\ No newline at end of file
diff --git a/Libs/DF/timeline.lua b/Libs/DF/timeline.lua
new file mode 100644
index 000000000..afb10ee8c
--- /dev/null
+++ b/Libs/DF/timeline.lua
@@ -0,0 +1,907 @@
+
+local detailsFramework = _G ["DetailsFramework"]
+if (not detailsFramework or not DetailsFrameworkCanLoad) then
+ return
+end
+
+local _
+--lua locals
+local rawset = rawset --lua local
+local rawget = rawget --lua local
+local setmetatable = setmetatable --lua local
+local unpack = table.unpack or unpack --lua local
+local type = type --lua local
+local floor = math.floor --lua local
+local loadstring = loadstring --lua local
+local CreateFrame = CreateFrame
+
+-- TWW compatibility:
+local GetSpellInfo = GetSpellInfo or function(spellID) if not spellID then return nil end local si = C_Spell.GetSpellInfo(spellID) if si then return si.name, nil, si.iconID, si.castTime, si.minRange, si.maxRange, si.spellID, si.originalIconID end end
+local GetNumSpellTabs = GetNumSpellTabs or C_SpellBook.GetNumSpellBookSkillLines
+local GetSpellTabInfo = GetSpellTabInfo or function(tabLine) local skillLine = C_SpellBook.GetSpellBookSkillLineInfo(tabLine) if skillLine then return skillLine.name, skillLine.iconID, skillLine.itemIndexOffset, skillLine.numSpellBookItems, skillLine.isGuild, skillLine.offSpecID end end
+local SPELLBOOK_BANK_PLAYER = Enum.SpellBookSpellBank and Enum.SpellBookSpellBank.Player or "player"
+local SpellBookItemTypeMap = Enum.SpellBookItemType and {[Enum.SpellBookItemType.Spell] = "SPELL", [Enum.SpellBookItemType.None] = "NONE", [Enum.SpellBookItemType.Flyout] = "FLYOUT", [Enum.SpellBookItemType.FutureSpell] = "FUTURESPELL", [Enum.SpellBookItemType.PetAction] = "PETACTION" } or {}
+local GetSpellBookItemInfo = GetSpellBookItemInfo or function(...) local si = C_SpellBook.GetSpellBookItemInfo(...) if si then return SpellBookItemTypeMap[si.itemType] or "NONE", (si.itemType == Enum.SpellBookItemType.Flyout or si.itemType == Enum.SpellBookItemType.PetAction) and si.actionID or si.spellID or si.actionID, si end end
+local GetSpellBookItemTexture = GetSpellBookItemTexture or function(...) return C_SpellBook.GetSpellBookItemTexture(...) end
+local GetSpellTexture = GetSpellTexture or function(...) return C_Spell.GetSpellTexture(...) end
+
+local IS_WOW_PROJECT_MAINLINE = WOW_PROJECT_ID == WOW_PROJECT_MAINLINE
+local IS_WOW_PROJECT_NOT_MAINLINE = WOW_PROJECT_ID ~= WOW_PROJECT_MAINLINE
+local IS_WOW_PROJECT_CLASSIC_ERA = WOW_PROJECT_ID == WOW_PROJECT_CLASSIC
+
+local CastInfo = detailsFramework.CastInfo
+
+local PixelUtil = PixelUtil or DFPixelUtil
+
+local UnitGroupRolesAssigned = detailsFramework.UnitGroupRolesAssigned
+
+local cleanfunction = function() end
+local APIFrameFunctions
+
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+--horizontal scroll frame
+
+---@class df_timeline_options : table
+---@field width number
+---@field height number
+---@field line_height number
+---@field line_padding number
+---@field show_elapsed_timeline boolean
+---@field elapsed_timeline_height number
+---@field header_width number
+---@field pixels_per_second number
+---@field scale_min number
+---@field scale_max number
+---@field backdrop backdrop
+---@field backdrop_color number[]
+---@field backdrop_color_highlight number[]
+---@field backdrop_border_color number[]
+---@field slider_backdrop backdrop
+---@field slider_backdrop_color number[]
+---@field slider_backdrop_border_color number[]
+---@field title_template string "ORANGE_FONT_TEMPLATE"
+---@field text_tempate string "OPTIONS_FONT_TEMPLATE"
+---@field on_enter fun(self:frame)
+---@field on_leave fun(self:frame)
+---@field block_on_enter fun(self:frame)
+---@field block_on_leave fun(self:frame)
+local timeline_options = {
+ width = 400,
+ height = 700,
+ line_height = 20,
+ line_padding = 1,
+
+ show_elapsed_timeline = true,
+ elapsed_timeline_height = 20,
+
+ --space to put the player/spell name and icons
+ header_width = 150,
+
+ --how many pixels will be use to represent 1 second
+ pixels_per_second = 20,
+
+ scale_min = 0.15,
+ scale_max = 1,
+
+ backdrop = {edgeFile = [[Interface\Buttons\WHITE8X8]], edgeSize = 1, bgFile = [[Interface\Tooltips\UI-Tooltip-Background]], tileSize = 64, tile = true},
+ backdrop_color = {0, 0, 0, 0.2},
+ backdrop_color_highlight = {.2, .2, .2, 0.4},
+ backdrop_border_color = {0.1, 0.1, 0.1, .2},
+
+ slider_backdrop = {edgeFile = [[Interface\Buttons\WHITE8X8]], edgeSize = 1, bgFile = [[Interface\Tooltips\UI-Tooltip-Background]], tileSize = 64, tile = true},
+ slider_backdrop_color = {0, 0, 0, 0.2},
+ slider_backdrop_border_color = {0.1, 0.1, 0.1, .2},
+
+ title_template = "ORANGE_FONT_TEMPLATE",
+ text_tempate = "OPTIONS_FONT_TEMPLATE",
+
+ on_enter = function(self)
+ self:SetBackdropColor(unpack(self.backdrop_color_highlight))
+ end,
+ on_leave = function(self)
+ self:SetBackdropColor(unpack(self.backdrop_color))
+ end,
+
+ block_on_enter = function(self)
+
+ end,
+ block_on_leave = function(self)
+
+ end,
+}
+
+---@class df_elapsedtime_options : table
+---@field backdrop backdrop
+---@field backdrop_color number[]
+---@field text_color number[]
+---@field text_size number
+---@field text_font string
+---@field text_outline outline
+---@field height number
+---@field distance number
+---@field distance_min number
+---@field draw_line boolean
+---@field draw_line_color number[]
+---@field draw_line_thickness number
+local elapsedtime_frame_options = {
+ backdrop = {bgFile = [[Interface\Tooltips\UI-Tooltip-Background]], tileSize = 64, tile = true},
+ backdrop_color = {.3, .3, .3, .7},
+
+ text_color = {1, 1, 1, 1},
+ text_size = 12,
+ text_font = "Arial Narrow",
+ text_outline = "NONE",
+
+ height = 20,
+
+ distance = 200, --distance in pixels between each label informing the time
+ distance_min = 50, --minimum distance in pixels
+ draw_line = true, --if true it'll draw a vertical line to represent a segment
+ draw_line_color = {1, 1, 1, 0.2},
+ draw_line_thickness = 1,
+}
+
+---@class df_elapsedtime_label : fontstring
+---@field line texture
+
+---@class df_elapsedtime_mixin : table
+---@field GetLabel fun(self:df_elapsedtime, index:number):fontstring
+---@field Reset fun(self:df_elapsedtime)
+---@field Refresh fun(self:df_elapsedtime, elapsedTime:number, scale:number)
+detailsFramework.TimeLineElapsedTimeFunctions = {
+ --get a label and update its appearance
+ GetLabel = function(self, index)
+ ---@type df_elapsedtime_label
+ local label = self.labels[index]
+
+ if (not label) then
+ label = self:CreateFontString(nil, "artwork", "GameFontNormal")
+ ---@cast label df_elapsedtime_label
+ label.line = self:CreateTexture(nil, "artwork")
+ label.line:SetColorTexture(1, 1, 1)
+ label.line:SetPoint("topleft", label, "bottomleft", 0, -2)
+ self.labels[index] = label
+ end
+
+ detailsFramework:SetFontColor(label, self.options.text_color)
+ detailsFramework:SetFontSize(label, self.options.text_size)
+ detailsFramework:SetFontFace(label, self.options.text_font)
+ detailsFramework:SetFontOutline(label, self.options.text_outline)
+
+ if (self.options.draw_line) then
+ label.line:SetVertexColor(unpack(self.options.draw_line_color))
+ label.line:SetWidth(self.options.draw_line_thickness)
+ label.line:Show()
+ else
+ label.line:Hide()
+ end
+
+ return label
+ end,
+
+ Reset = function(self)
+ for i = 1, #self.labels do
+ self.labels[i]:Hide()
+ end
+ end,
+
+ Refresh = function(self, elapsedTime, scale)
+ local parent = self:GetParent()
+
+ self:SetHeight(self.options.height)
+ local effectiveArea = self:GetWidth() --already scaled down width
+ local pixelPerSecond = elapsedTime / effectiveArea --how much 1 pixels correlate to time
+
+ local distance = self.options.distance --pixels between each segment
+ local minDistance = self.options.distance_min --min pixels between each segment
+
+ --scale the distance between each label showing the time with the parent's scale
+ distance = distance * scale
+ distance = max(distance, minDistance)
+
+ local amountSegments = ceil (effectiveArea / distance)
+
+ for i = 1, amountSegments do
+ ---@type df_elapsedtime_label
+ local label = self:GetLabel(i)
+ local xOffset = distance * (i - 1)
+ label:SetPoint("left", self, "left", xOffset, 0)
+
+ local secondsOfTime = pixelPerSecond * xOffset
+
+ label:SetText(detailsFramework:IntegerToTimer(floor(secondsOfTime)))
+
+ if (label.line:IsShown()) then
+ label.line:SetHeight(parent:GetParent():GetHeight())
+ end
+
+ label:Show()
+ end
+ end,
+}
+
+---@class df_elapsedtime : frame, df_elapsedtime_mixin, df_optionsmixin
+---@field labels table
+
+---creates a frame to show the elapsed time in a row
+---@param parent frame
+---@param name string?
+---@param options df_elapsedtime_options?
+function detailsFramework:CreateElapsedTimeFrame(parent, name, options)
+ local elapsedTimeFrame = CreateFrame("frame", name, parent, "BackdropTemplate")
+
+ detailsFramework:Mixin(elapsedTimeFrame, detailsFramework.OptionsFunctions)
+ detailsFramework:Mixin(elapsedTimeFrame, detailsFramework.LayoutFrame)
+ detailsFramework:Mixin(elapsedTimeFrame, detailsFramework.TimeLineElapsedTimeFunctions)
+
+ elapsedTimeFrame:BuildOptionsTable(elapsedtime_frame_options, options)
+
+ elapsedTimeFrame:SetBackdrop(elapsedTimeFrame.options.backdrop)
+ elapsedTimeFrame:SetBackdropColor(unpack(elapsedTimeFrame.options.backdrop_color))
+
+ elapsedTimeFrame.labels = {}
+
+ return elapsedTimeFrame
+end
+
+---@class df_timeline_block_data : table
+---@field [1] number timeInSeconds
+---@field [2] number length
+---@field [3] boolean isAura
+---@field [4] number auraDuration
+---@field [5] number blockSpellId
+---@field payload any
+
+---@class df_timeline_linedata : table
+---@field spellId number
+---@field icon any
+---@field coords number[]?
+---@field text string?
+---@field timeline df_timeline_block_data[]
+
+---@class df_timeline_scrolldata : table
+---@field length number
+---@field defaultColor number[]
+---@field useIconOnBlocks boolean
+---@field lines df_timeline_linedata[]
+
+---@class df_timeline_line_blockinfo : table
+---@field time number
+---@field duration number
+---@field spellId number
+---@field payload any
+
+---@class df_timeline_line_block : frame
+---@field icon texture
+---@field text fontstring
+---@field background texture
+---@field auraLength texture
+---@field info df_timeline_line_blockinfo
+
+---@class df_timeline_line_mixin : frame
+---@field lineHeader frame
+---@field blocks df_timeline_line_block[]
+---@field SetBlock fun(self:df_timeline_line, index:number, blockInfo:table)
+---@field GetBlock fun(self:df_timeline_line, index:number):df_timeline_line_block
+---@field SetBlocksFromData fun(self:df_timeline_line)
+---@field Reset fun(self:df_timeline_line)
+detailsFramework.TimeLine_LineMixin = {
+ --self is the line
+ SetBlock = function(self, index, blockInfo)
+ --get the block information
+ --see what is the current scale
+ --adjust the block position
+
+ local block = self:GetBlock(index)
+
+ --need:
+ --the total time of the timeline
+ --the current scale of the timeline
+ --the elapsed time of this block
+ --icon of the block
+ --text
+ --background color
+
+ end,
+
+ SetBlocksFromData = function(self)
+ local parent = self:GetParent():GetParent()
+ local data = parent.data
+ local defaultColor = parent.defaultColor --guarantee to have a value
+
+ self:Show()
+
+ --none of these values are scaled, need to calculate
+ local pixelPerSecond = parent.pixelPerSecond
+ local totalLength = parent.totalLength
+ local scale = parent.currentScale
+
+ pixelPerSecond = pixelPerSecond * scale
+
+ local headerWidth = parent.headerWidth
+
+ --dataIndex stores which line index from the data this line will use
+ --lineData store members: .text .icon .timeline
+ ---@type df_timeline_linedata
+ local lineData = data.lines[self.dataIndex]
+
+ self.spellId = lineData.spellId
+
+ --if there's an icon, anchor the text at the right side of the icon
+ --this is the title and icon of the title
+ if (lineData.icon) then
+ self.icon:SetTexture(lineData.icon)
+ if (lineData.coords) then
+ self.icon:SetTexCoord(unpack(lineData.coords))
+ else
+ self.icon:SetTexCoord(.1, .9, .1, .9)
+ end
+ self.text:SetText(lineData.text or "")
+ self.text:SetPoint("left", self.icon.widget, "right", 2, 0)
+ else
+ self.icon:SetTexture(nil)
+ self.text:SetText(lineData.text or "")
+ self.text:SetPoint("left", self, "left", 2, 0)
+ end
+
+ if (self.dataIndex % 2 == 1) then
+ self:SetBackdropColor(0, 0, 0, 0)
+ else
+ local r, g, b, a = unpack(self.backdrop_color)
+ self:SetBackdropColor(r, g, b, a)
+ end
+
+ self:SetWidth(5000)
+
+ local timelineData = lineData.timeline
+ local spellId = lineData.spellId
+ local useIconOnBlock = data.useIconOnBlocks
+
+ local baseFrameLevel = parent:GetFrameLevel() + 10
+
+ for i = 1, #timelineData do
+ local blockInfo = timelineData[i]
+
+ local timeInSeconds = blockInfo[1]
+ local length = blockInfo[2]
+ local isAura = blockInfo[3]
+ local auraDuration = blockInfo[4]
+ local blockSpellId = blockInfo[5]
+
+ local payload = blockInfo.payload
+
+ local xOffset = pixelPerSecond * timeInSeconds
+ local width = pixelPerSecond * length
+
+ if (timeInSeconds < -0.2) then
+ xOffset = xOffset / 2.5
+ end
+
+ local block = self:GetBlock(i)
+ block:Show()
+ block:SetFrameLevel(baseFrameLevel + i)
+
+ PixelUtil.SetPoint(block, "left", self, "left", xOffset + headerWidth, 0)
+
+ block.info.spellId = blockSpellId or spellId
+ block.info.time = timeInSeconds
+ block.info.duration = auraDuration
+ block.info.payload = payload
+
+ if (useIconOnBlock) then
+ local iconTexture = lineData.icon
+ if (blockSpellId) then
+ iconTexture = GetSpellTexture(blockSpellId)
+ end
+
+ block.icon:SetTexture(iconTexture)
+ block.icon:SetTexCoord(.1, .9, .1, .9)
+ block.icon:SetAlpha(.834)
+ block.icon:SetSize(self:GetHeight(), self:GetHeight())
+
+ if (timeInSeconds < -0.2) then
+ block.icon:SetDesaturated(true)
+ else
+ block.icon:SetDesaturated(false)
+ end
+
+ PixelUtil.SetSize(block, self:GetHeight(), self:GetHeight())
+
+ if (isAura) then
+ block.auraLength:Show()
+ block.auraLength:SetWidth(pixelPerSecond * isAura)
+ block:SetWidth(max(pixelPerSecond * isAura, 16))
+ else
+ block.auraLength:Hide()
+ end
+
+ block.background:SetVertexColor(0, 0, 0, 0)
+ else
+ block.background:SetVertexColor(0, 0, 0, 0)
+ PixelUtil.SetSize(block, max(width, 16), self:GetHeight())
+ block.auraLength:Hide()
+ end
+ end
+ end,
+
+ GetBlock = function(self, index)
+ local block = self.blocks[index]
+ if (not block) then
+ block = CreateFrame("button", nil, self, "BackdropTemplate")
+ block:SetMouseClickEnabled(false)
+ self.blocks[index] = block
+
+ local background = block:CreateTexture(nil, "background")
+ background:SetColorTexture(1, 1, 1, 1)
+ local icon = block:CreateTexture(nil, "artwork")
+ local text = block:CreateFontString(nil, "artwork")
+ local auraLength = block:CreateTexture(nil, "border")
+
+ background:SetAllPoints()
+ icon:SetPoint("left")
+ text:SetPoint("left", icon, "left", 2, 0)
+ auraLength:SetPoint("topleft", icon, "topleft", 0, 0)
+ auraLength:SetPoint("bottomleft", icon, "bottomleft", 0, 0)
+ auraLength:SetColorTexture(1, 1, 1, 1)
+ auraLength:SetVertexColor(1, 1, 1, 0.1)
+
+ block.icon = icon
+ block.text = text
+ block.background = background
+ block.auraLength = auraLength
+
+ block:SetScript("OnEnter", self:GetParent():GetParent().options.block_on_enter)
+ block:SetScript("OnLeave", self:GetParent():GetParent().options.block_on_leave)
+
+ block:SetMouseClickEnabled(false)
+ block.info = {}
+ end
+
+ return block
+ end,
+
+ Reset = function(self)
+ --attention, it doesn't reset icon texture, text and background color
+ for i = 1, #self.blocks do
+ self.blocks[i]:Hide()
+ end
+ self:Hide()
+ end,
+}
+
+---@class df_timeline_line : frame, df_timeline_line_mixin
+---@field spellId number
+---@field icon df_image
+---@field text df_label
+---@field dataIndex number
+---@field backdrop_color table
+---@field backdrop_color_highlight table
+
+---@class df_timeline : scrollframe, df_timeline_mixin, df_optionsmixin, df_framelayout
+---@field body frame
+---@field elapsedTimeFrame df_elapsedtime
+---@field horizontalSlider slider
+---@field scaleSlider slider
+---@field verticalSlider slider
+---@field currentScale number
+---@field data df_timeline_scrolldata
+---@field lines df_timeline_line[]
+---@field options table
+---@field pixelPerSecond number
+---@field totalLength number
+---@field defaultColor table
+---@field headerWidth number
+
+---@class df_timeline_mixin : table
+---@field GetLine fun(self:df_timeline, index:number):df_timeline_line
+---@field ResetAllLines fun(self:df_timeline)
+---@field RefreshTimeLine fun(self:df_timeline)
+---@field SetData fun(self:df_timeline, data:table)
+---@field GetData fun(self:df_timeline):table
+detailsFramework.TimeLineMixin = {
+ GetLine = function(self, index)
+ local line = self.lines[index]
+ if (not line) then
+ --create a new line
+ ---@type df_timeline_line
+ line = CreateFrame("frame", "$parentLine" .. index, self.body, "BackdropTemplate")
+ detailsFramework:Mixin(line, detailsFramework.TimeLine_LineMixin)
+ self.lines[index] = line
+
+ local lineHeader = CreateFrame("frame", nil, line, "BackdropTemplate")
+ lineHeader:SetPoint("topleft", line, "topleft", 0, 0)
+ lineHeader:SetPoint("bottomleft", line, "bottomleft", 0, 0)
+ lineHeader:SetScript("OnEnter", self.options.header_on_enter)
+ lineHeader:SetScript("OnLeave", self.options.header_on_leave)
+
+ line.lineHeader = lineHeader
+
+ --store the individual textures that shows the timeline information
+ line.blocks = {}
+
+ if (self.options.show_elapsed_timeline) then
+ line:SetPoint("topleft", self.body, "topleft", 1, -((index-1) * (self.options.line_height + 1)) - 2 - self.options.elapsed_timeline_height)
+ else
+ line:SetPoint("topleft", self.body, "topleft", 1, -((index-1) * (self.options.line_height + 1)) - 1)
+ end
+ line:SetSize(1, self.options.line_height) --width is set when updating the frame
+
+ line:SetScript("OnEnter", self.options.on_enter)
+ line:SetScript("OnLeave", self.options.on_leave)
+ line:SetMouseClickEnabled(false)
+
+ line:SetBackdrop(self.options.backdrop)
+ line:SetBackdropColor(unpack(self.options.backdrop_color))
+ line:SetBackdropBorderColor(unpack(self.options.backdrop_border_color))
+
+ local icon = detailsFramework:CreateImage(line, "", self.options.line_height, self.options.line_height)
+ icon:SetPoint("left", line, "left", 2, 0)
+ line.icon = icon
+
+ local text = detailsFramework:CreateLabel(line, "", detailsFramework:GetTemplate("font", self.options.title_template))
+ text:SetPoint("left", icon.widget, "right", 2, 0)
+ line.text = text
+
+ line.backdrop_color = self.options.backdrop_color or {.1, .1, .1, .3}
+ line.backdrop_color_highlight = self.options.backdrop_color_highlight or {.3, .3, .3, .5}
+ end
+
+ return line
+ end,
+
+ ResetAllLines = function(self)
+ for i = 1, #self.lines do
+ self.lines[i]:Reset()
+ end
+ end,
+
+ --todo
+ --make the on enter and leave tooltips
+ --set icons and texts
+ --skin the sliders
+
+ RefreshTimeLine = function(self)
+ --debug
+ --self.currentScale = 1
+
+ --calculate the total width
+ local pixelPerSecond = self.options.pixels_per_second
+ local totalLength = self.data.length or 1
+ local currentScale = self.currentScale
+
+ self.scaleSlider:Enable()
+
+ --how many pixels represent 1 second
+ local bodyWidth = totalLength * pixelPerSecond * currentScale
+ self.body:SetWidth(bodyWidth + self.options.header_width)
+ self.body.effectiveWidth = bodyWidth
+
+ --reduce the default canvas size from the body with and don't allow the max value be negative
+ local newMaxValue = max(bodyWidth - (self:GetWidth() - self.options.header_width), 0)
+
+ --adjust the scale slider range
+ local oldMin, oldMax = self.horizontalSlider:GetMinMaxValues()
+ self.horizontalSlider:SetMinMaxValues(0, newMaxValue)
+ self.horizontalSlider:SetValue(detailsFramework:MapRangeClamped(oldMin, oldMax, 0, newMaxValue, self.horizontalSlider:GetValue()))
+
+ local defaultColor = self.data.defaultColor or {1, 1, 1, 1}
+
+ --cache values
+ self.pixelPerSecond = pixelPerSecond
+ self.totalLength = totalLength
+ self.defaultColor = defaultColor
+ self.headerWidth = self.options.header_width
+
+ --calculate the total height
+ local lineHeight = self.options.line_height
+ local linePadding = self.options.line_padding
+
+ local bodyHeight = (lineHeight + linePadding) * #self.data.lines
+ self.body:SetHeight(bodyHeight)
+ self.verticalSlider:SetMinMaxValues(0, max(bodyHeight - self:GetHeight(), 0))
+ self.verticalSlider:SetValue(0)
+
+ --refresh lines
+ self:ResetAllLines()
+ for i = 1, #self.data.lines do
+ local line = self:GetLine(i)
+ line.dataIndex = i --this index is used inside the line update function to know which data to get
+ line.lineHeader:SetWidth(self.options.header_width)
+ line:SetBlocksFromData() --the function to update runs within the line object
+ end
+
+ --refresh elapsed time frame
+ --the elapsed frame must have a width before the refresh function is called
+ self.elapsedTimeFrame:ClearAllPoints()
+ self.elapsedTimeFrame:SetPoint("topleft", self.body, "topleft", self.options.header_width, 0)
+ self.elapsedTimeFrame:SetPoint("topright", self.body, "topright", 0, 0)
+ self.elapsedTimeFrame:Reset()
+
+ self.elapsedTimeFrame:Refresh(self.data.length, self.currentScale)
+ end,
+
+ ---@param self df_timeline
+ ---@param data df_timeline_scrolldata
+ SetData = function(self, data)
+ self.data = data
+ self:RefreshTimeLine()
+ end,
+
+ ---@param self df_timeline
+ ---@return df_timeline_scrolldata
+ GetData = function(self)
+ return self.data
+ end,
+
+}
+
+detailsFramework.LineIndicatorMixin = {
+ LineIndicatorConstructor = function(self)
+ self.nextLineIndicatorIndex = 1
+ self.lineIndicators = {}
+ end,
+
+ --hide all indicators and clear their points
+ ResetLineIndicators = function(self)
+ self.nextLineIndicatorIndex = 1
+ for i = 1, #self.lineIndicators do
+ local thisIndicator = self.lineIndicators[i]
+ thisIndicator:Hide()
+ thisIndicator:ClearAllPoints()
+ end
+ end,
+
+ CreateLineIndicator = function(self, index)
+ local parentName = self:GetName()
+ local indicatorName = parentName and parentName .. "LineIndicator" .. index
+
+ --self.body is the scrollChild
+ local indicator = CreateFrame("button", indicatorName, self.body, "BackdropTemplate")
+ indicator:SetSize(1, self:GetHeight())
+
+ local texture = indicator:CreateTexture(nil, "background")
+ texture:SetColorTexture(1, 1, 1, 1)
+ texture:SetAllPoints()
+
+ indicator.Texture = texture
+
+ return indicator
+ end,
+
+ GetLineIndicator = function(self)
+ assert(self.lineIndicators, "GetLineIndicator(): LineIndicatorConstructor() not called.")
+ local thisIndicator = self.lineIndicators[self.nextLineIndicatorIndex]
+
+ if (not thisIndicator) then
+ thisIndicator = self:CreateLineIndicator(self.nextLineIndicatorIndex)
+ self.lineIndicators[self.nextLineIndicatorIndex] = thisIndicator
+ self.nextLineIndicatorIndex = self.nextLineIndicatorIndex + 1
+ end
+
+ return thisIndicator
+ end,
+
+ SetLineIndicatorPosition = function(self, x)
+ self:SetPoint("left", self:GetParent(), "left", x, 0)
+ end,
+}
+
+---creates a scrollable panel with vertical, horizontal and scale sliders to show a timeline
+---also creates a frame for the elapsed timeline at the top, it shows the time in seconds
+---@param parent frame
+---@param name string
+---@param timelineOptions df_timeline_options
+---@param elapsedtimeOptions df_elapsedtime_options
+---@return df_timeline
+function detailsFramework:CreateTimeLineFrame(parent, name, timelineOptions, elapsedtimeOptions)
+ local width = timelineOptions and timelineOptions.width or timeline_options.width
+ local height = timelineOptions and timelineOptions.height or timeline_options.height
+ local scrollWidth = 800 --placeholder until the timeline receives data
+ local scrollHeight = 800 --placeholder until the timeline receives data
+
+ ---@type df_timeline
+ local frameCanvas = CreateFrame("scrollframe", name, parent, "BackdropTemplate")
+
+ detailsFramework:Mixin(frameCanvas, detailsFramework.TimeLineMixin)
+ detailsFramework:Mixin(frameCanvas, detailsFramework.OptionsFunctions)
+ detailsFramework:Mixin(frameCanvas, detailsFramework.LayoutFrame)
+
+ --this table is changed by SetData()
+ frameCanvas.data = {} --placeholder
+ frameCanvas.lines = {}
+
+ --store vertical lines created by CreateIndicator()
+ frameCanvas.lineIndicators = {}
+
+ frameCanvas.currentScale = 0.5
+ frameCanvas:SetSize(width, height)
+
+ detailsFramework:ApplyStandardBackdrop(frameCanvas)
+
+ local frameBody = CreateFrame("frame", nil, frameCanvas, "BackdropTemplate")
+ frameBody:SetSize(scrollWidth, scrollHeight)
+
+ frameCanvas:SetScrollChild(frameBody)
+ frameCanvas.body = frameBody
+
+ frameCanvas:BuildOptionsTable(timeline_options, timelineOptions)
+
+ --create elapsed time frame
+ frameCanvas.elapsedTimeFrame = detailsFramework:CreateElapsedTimeFrame(frameBody, frameCanvas:GetName() and frameCanvas:GetName() .. "ElapsedTimeFrame", elapsedtimeOptions)
+
+ local thumbColor = 0.95
+ local scrollBackgroudColor = {0.05, 0.05, 0.05, 0.7}
+
+ --create horizontal slider
+ local horizontalSlider = CreateFrame("slider", frameCanvas:GetName() .. "HorizontalSlider", parent, "BackdropTemplate")
+ horizontalSlider.bg = horizontalSlider:CreateTexture(nil, "background")
+ horizontalSlider.bg:SetAllPoints(true)
+ horizontalSlider.bg:SetColorTexture(unpack(scrollBackgroudColor))
+ frameCanvas.horizontalSlider = horizontalSlider
+
+ horizontalSlider:SetBackdrop(frameCanvas.options.slider_backdrop)
+ horizontalSlider:SetBackdropColor(unpack(frameCanvas.options.slider_backdrop_color))
+ horizontalSlider:SetBackdropBorderColor(unpack(frameCanvas.options.slider_backdrop_border_color))
+
+ horizontalSlider.thumb = horizontalSlider:CreateTexture(nil, "OVERLAY")
+ horizontalSlider.thumb:SetTexture("Interface\\Buttons\\UI-ScrollBar-Knob")
+ horizontalSlider.thumb:SetSize(24, 24)
+ horizontalSlider.thumb:SetVertexColor(thumbColor, thumbColor, thumbColor, 0.95)
+ horizontalSlider:SetThumbTexture(horizontalSlider.thumb)
+
+ horizontalSlider:SetOrientation("horizontal")
+ horizontalSlider:SetSize(width + 20, 20)
+ horizontalSlider:SetPoint("topleft", frameCanvas, "bottomleft")
+ horizontalSlider:SetMinMaxValues(0, scrollWidth)
+ horizontalSlider:SetValue(0)
+ horizontalSlider:SetScript("OnValueChanged", function(self)
+ local _, maxValue = horizontalSlider:GetMinMaxValues()
+ local stepValue = ceil(ceil(self:GetValue() * maxValue) / max(maxValue, SMALL_FLOAT))
+ if (stepValue ~= horizontalSlider.currentValue) then
+ horizontalSlider.currentValue = stepValue
+ frameCanvas:SetHorizontalScroll(stepValue)
+ end
+ end)
+
+ --create scale slider
+ local scaleSlider = CreateFrame("slider", frameCanvas:GetName() .. "ScaleSlider", parent, "BackdropTemplate")
+ scaleSlider.bg = scaleSlider:CreateTexture(nil, "background")
+ scaleSlider.bg:SetAllPoints(true)
+ scaleSlider.bg:SetColorTexture(unpack(scrollBackgroudColor))
+ scaleSlider:Disable()
+ frameCanvas.scaleSlider = scaleSlider
+
+ scaleSlider:SetBackdrop(frameCanvas.options.slider_backdrop)
+ scaleSlider:SetBackdropColor(unpack(frameCanvas.options.slider_backdrop_color))
+ scaleSlider:SetBackdropBorderColor(unpack(frameCanvas.options.slider_backdrop_border_color))
+
+ scaleSlider.thumb = scaleSlider:CreateTexture(nil, "OVERLAY")
+ scaleSlider.thumb:SetTexture("Interface\\Buttons\\UI-ScrollBar-Knob")
+ scaleSlider.thumb:SetSize(24, 24)
+ scaleSlider.thumb:SetVertexColor(thumbColor, thumbColor, thumbColor, 0.95)
+ scaleSlider:SetThumbTexture(scaleSlider.thumb)
+
+ scaleSlider:SetOrientation("horizontal")
+ scaleSlider:SetSize(width + 20, 20)
+ scaleSlider:SetPoint("topleft", horizontalSlider, "bottomleft", 0, -2)
+ scaleSlider:SetMinMaxValues(frameCanvas.options.scale_min, frameCanvas.options.scale_max)
+ scaleSlider:SetValue(detailsFramework:GetRangeValue(frameCanvas.options.scale_min, frameCanvas.options.scale_max, 0.5))
+
+ scaleSlider:SetScript("OnValueChanged", function(self)
+ local stepValue = ceil(self:GetValue() * 100) / 100
+ if (stepValue ~= frameCanvas.currentScale) then
+ local current = stepValue
+ frameCanvas.currentScale = stepValue
+ frameCanvas:RefreshTimeLine()
+ end
+ end)
+
+ --create vertical slider
+ local verticalSlider = CreateFrame("slider", frameCanvas:GetName() .. "VerticalSlider", parent, "BackdropTemplate")
+ verticalSlider.bg = verticalSlider:CreateTexture(nil, "background")
+ verticalSlider.bg:SetAllPoints(true)
+ verticalSlider.bg:SetColorTexture(unpack(scrollBackgroudColor))
+ frameCanvas.verticalSlider = verticalSlider
+
+ verticalSlider:SetBackdrop(frameCanvas.options.slider_backdrop)
+ verticalSlider:SetBackdropColor(unpack(frameCanvas.options.slider_backdrop_color))
+ verticalSlider:SetBackdropBorderColor(unpack(frameCanvas.options.slider_backdrop_border_color))
+
+ verticalSlider.thumb = verticalSlider:CreateTexture(nil, "OVERLAY")
+ verticalSlider.thumb:SetTexture("Interface\\Buttons\\UI-ScrollBar-Knob")
+ verticalSlider.thumb:SetSize(24, 24)
+ verticalSlider.thumb:SetVertexColor(thumbColor, thumbColor, thumbColor, 0.95)
+ verticalSlider:SetThumbTexture(verticalSlider.thumb)
+
+ verticalSlider:SetOrientation("vertical")
+ verticalSlider:SetSize(20, height - 2)
+ verticalSlider:SetPoint("topleft", frameCanvas, "topright", 0, 0)
+ verticalSlider:SetMinMaxValues(0, scrollHeight)
+ verticalSlider:SetValue(0)
+ verticalSlider:SetScript("OnValueChanged", function(self)
+ frameCanvas:SetVerticalScroll(self:GetValue())
+ end)
+
+ --mouse scroll
+ frameCanvas:EnableMouseWheel(true)
+ frameCanvas:SetScript("OnMouseWheel", function(self, delta)
+ local minValue, maxValue = horizontalSlider:GetMinMaxValues()
+ local currentHorizontal = horizontalSlider:GetValue()
+
+ if (IsShiftKeyDown() and delta < 0) then
+ local amountToScroll = frameBody:GetHeight() / 20
+ verticalSlider:SetValue(verticalSlider:GetValue() + amountToScroll)
+
+ elseif (IsShiftKeyDown() and delta > 0) then
+ local amountToScroll = frameBody:GetHeight() / 20
+ verticalSlider:SetValue(verticalSlider:GetValue() - amountToScroll)
+
+ elseif (IsControlKeyDown() and delta > 0) then
+ scaleSlider:SetValue(min(scaleSlider:GetValue() + 0.1, 1))
+
+ elseif (IsControlKeyDown() and delta < 0) then
+ scaleSlider:SetValue(max(scaleSlider:GetValue() - 0.1, 0.15))
+
+ elseif (delta < 0 and currentHorizontal < maxValue) then
+ local amountToScroll = frameBody:GetWidth() / 20
+ horizontalSlider:SetValue(currentHorizontal + amountToScroll)
+
+ elseif (delta > 0 and maxValue > 1) then
+ local amountToScroll = frameBody:GetWidth() / 20
+ horizontalSlider:SetValue(currentHorizontal - amountToScroll)
+
+ end
+ end)
+
+ --mouse drag
+ frameBody:SetScript("OnMouseDown", function(self, button)
+ local x = GetCursorPosition()
+ self.MouseX = x
+
+ frameBody:SetScript("OnUpdate", function(self, deltaTime)
+ local x = GetCursorPosition()
+ local deltaX = self.MouseX - x
+ local current = horizontalSlider:GetValue()
+ horizontalSlider:SetValue(current +(deltaX * 1.2) *((IsShiftKeyDown() and 2) or(IsAltKeyDown() and 0.5) or 1))
+ self.MouseX = x
+ end)
+ end)
+
+ frameBody:SetScript("OnMouseUp", function(self, button)
+ frameBody:SetScript("OnUpdate", nil)
+ end)
+
+ return frameCanvas
+end
+
+
+
+--[=[
+local f = CreateFrame("frame", "TestFrame", UIParent)
+f:SetPoint("center")
+f:SetSize(900, 420)
+f:SetBackdrop({bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", tile = true, tileSize = 16, insets = {left = 1, right = 1, top = 0, bottom = 1}})
+
+local scroll = DF:CreateTimeLineFrame (f, "$parentTimeLine", {width = 880, height = 400})
+scroll:SetPoint("topleft", f, "topleft", 0, 0)
+
+--need fake data to test fills
+scroll:SetData ({
+ length = 360,
+ defaultColor = {1, 1, 1, 1},
+ lines = {
+ {text = "player 1", icon = "", timeline = {
+ --each table here is a block shown in the line
+ --is an indexed table with: [1] time [2] length [3] color (if false, use the default) [4] text [5] icon [6] tooltip: if number = spellID tooltip, if table is text lines
+ {1, 10}, {13, 11}, {25, 7}, {36, 5}, {55, 18}, {76, 30}, {105, 20}, {130, 11}, {155, 11}, {169, 7}, {199, 16}, {220, 18}, {260, 10}, {290, 23}, {310, 30}, {350, 10}
+ }
+ }, --end of line 1
+ },
+})
+
+
+f:Hide()
+
+--scroll.body:SetScale(0.5)
+
+--]=]
\ No newline at end of file
diff --git a/classes/container_actors.lua b/classes/container_actors.lua
index 4c0f8fc9a..4a7b86456 100644
--- a/classes/container_actors.lua
+++ b/classes/container_actors.lua
@@ -775,6 +775,7 @@ unitNameTitles[#unitNameTitles+1] = unitNameTitles[1]:gsub(PET_TYPE_PET, PET_TYP
--does this actor has an owner? (a.k.a. is a pet)
elseif (ownerActorObject) then
+ local npcID = Details:GetNpcIdFromGuid(actorSerial)
actorObject.owner = ownerActorObject
actorObject.ownerName = ownerActorObject.nome
diff --git a/core/gears.lua b/core/gears.lua
index 53c4ba0e0..5de5a4c2d 100644
--- a/core/gears.lua
+++ b/core/gears.lua
@@ -547,1292 +547,6 @@ function Details222.Parser.GetState()
end
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
---storage stuff ~storage
-
----@class details_storage_unitresult : table
----@field total number
----@field itemLevel number
----@field classId number
-
----@class details_encounterkillinfo : table
----@field guild guildname
----@field time unixtime
----@field date date
----@field elapsed number
----@field HEALER table
----@field servertime unixtime
----@field DAMAGER table
-
----@class details_bosskillinfo : table
----@field kills number
----@field wipes number
----@field time_fasterkill number
----@field time_fasterkill_when unixtime
----@field time_incombat number
----@field dps_best number
----@field dps_best_when unixtime
----@field dps_best_raid number
----@field dps_best_raid_when unixtime
-
----@class details_storage : table
----@field VERSION number the database version
----@field normal table
----@field heroic table
----@field mythic table
----@field mythic_plus table
----@field saved_encounters table
----@field totalkills table>
-
----@class details_storage_feature : table
----@field diffNames string[] {"normal", "heroic", "mythic", "raidfinder"}
----@field OpenRaidStorage fun():details_storage
----@field HaveDataForEncounter fun(difficulty:string, encounterId:number, guildName:string|boolean):boolean
----@field GetBestFromGuild fun(difficulty:string, encounterId:number, role:role, dps:boolean, guildName:string):actorname, details_storage_unitresult, details_encounterkillinfo
----@field GetUnitGuildRank fun(difficulty:string, encounterId:number, role:role, guildName:guildname, unitName:actorname):number?, details_storage_unitresult?, details_encounterkillinfo?
----@field GetBestFromPlayer fun(difficulty:string, encounterId:number, role:role, dps:boolean, playerName:actorname):details_storage_unitresult, details_encounterkillinfo
----@field DBGuildSync fun()
-
-local CONST_ADDONNAME_DATASTORAGE = "Details_DataStorage"
-
-local diffNumberToName = Details222.storage.DiffIdToName
-
-local createStorageTables = function()
- local storageDatabase = DetailsDataStorage
-
- if (not storageDatabase and Details.CreateStorageDB) then
- storageDatabase = Details:CreateStorageDB()
- if (not storageDatabase) then
- return
- end
-
- elseif (not storageDatabase) then
- return
- end
-
- return storageDatabase
-end
-
----@return details_storage?
-function Details222.storage.OpenRaidStorage()
- --check if the storage is already loaded
- if (not C_AddOns.IsAddOnLoaded(CONST_ADDONNAME_DATASTORAGE)) then
- local loaded, reason = C_AddOns.LoadAddOn(CONST_ADDONNAME_DATASTORAGE)
- if (not loaded) then
- return
- end
- end
-
- --get the storage table
- local savedData = DetailsDataStorage
-
- if (not savedData and Details.CreateStorageDB) then
- savedData = Details:CreateStorageDB()
- if (not savedData) then
- return
- end
-
- elseif (not savedData) then
- return
- end
-
- return savedData
-end
-
----check if there is data for a specific encounter and difficulty, if a guildName is passed, check if there is data for the guild
----@param difficulty string
----@param encounterId number
----@param guildName string|boolean
----@return boolean bHasData
-function Details222.storage.HaveDataForEncounter(difficulty, encounterId, guildName)
- ---@type details_storage?
- local savedData = Details222.storage.OpenRaidStorage()
- if (not savedData) then
- return false
- end
-
- difficulty = diffNumberToName[difficulty] or difficulty
-
- if (guildName and type(guildName) == "boolean") then
- guildName = GetGuildInfo("player")
- end
-
- ---@type table
- local encountersTable = savedData[difficulty]
- if (encountersTable) then
- local allEncountersStored = encountersTable[encounterId]
- if (allEncountersStored) then
- --didn't requested a guild name, so just return 'we have data for this encounter'
- if (not guildName) then
- return true
- end
-
- --data for a specific guild is requested, check if there is data for the guild
- for index, encounterKillInfo in ipairs(allEncountersStored) do
- if (encounterKillInfo.guild == guildName) then
- return true
- end
- end
- end
- end
-
- return false
-end
-
----find the best unit from a specific role from a specific guild in a specific encounter and difficulty
----check all encounters saved for the guild and difficulty and return the unit with the best performance
----@param difficulty string
----@param encounterId number
----@param role role
----@param dps boolean?
----@param guildName string
----@return boolean|string playerName
----@return boolean|details_storage_unitresult storageUnitResult
----@return boolean|details_encounterkillinfo encounterKillInfo
-function Details222.storage.GetBestFromGuild(difficulty, encounterId, role, dps, guildName)
- ---@type details_storage?
- local savedData = Details222.storage.OpenRaidStorage()
-
- if (not savedData) then
- return false, false, false
- end
-
- if (not guildName) then
- guildName = GetGuildInfo("player")
- end
-
- if (not guildName) then
- if (Details.debug) then
- Details:Msg("(debug) GetBestFromGuild() guild name invalid.")
- end
- return false, false, false
- end
-
- local best = 0
- local bestDps = 0
- local bestEncounterKillInfo
- local bestUnitName
- local bestStorageResultTable
-
- if (not role) then
- role = "DAMAGER"
- end
-
- ---@type table
- local encountersTable = savedData[difficulty]
- if (encountersTable) then
- local allEncountersStored = encountersTable[encounterId]
- if (allEncountersStored) then
- for index, encounterKillInfo in ipairs(allEncountersStored) do
- if (encounterKillInfo.guild == guildName) then
- ---@type table
- local unitListFromRole = encounterKillInfo[role]
- if (unitListFromRole) then
- for unitName, storageUnitResult in pairs(unitListFromRole) do
- if (dps) then
- if (storageUnitResult.total / encounterKillInfo.elapsed > bestDps) then
- bestDps = storageUnitResult.total / encounterKillInfo.elapsed
- bestUnitName = unitName
- bestEncounterKillInfo = encounterKillInfo
- bestStorageResultTable = storageUnitResult
- end
- else
- if (storageUnitResult.total > best) then
- best = storageUnitResult.total
- bestUnitName = unitName
- bestEncounterKillInfo = encounterKillInfo
- bestStorageResultTable = storageUnitResult
- end
- end
- end
- end
- end
- end
- end
- end
-
- return bestUnitName, bestStorageResultTable, bestEncounterKillInfo
-end
-
----find and return the rank position of a unit among all other players guild
----the rank is based on the biggest total amount of damage or healing (role) done in a specific encounter and difficulty
----@param difficulty string
----@param encounterId number
----@param role role
----@param unitName actorname
----@param dps boolean?
----@param guildName guildname
----@return number positionIndex?
----@return details_storage_unitresult storageUnitResult?
----@return details_encounterkillinfo encounterKillInfo?
-function Details222.storage.GetUnitGuildRank(difficulty, encounterId, role, unitName, dps, guildName)
- ---@type details_storage?
- local savedData = Details222.storage.OpenRaidStorage()
-
- if (not savedData) then
- return
- end
-
- if (not guildName) then
- guildName = GetGuildInfo("player")
- end
-
- if (not guildName) then
- if (Details.debug) then
- Details:Msg("(debug) GetBestFromGuild() guild name invalid.")
- end
- return
- end
-
- if (not role) then
- role = "DAMAGER"
- end
-
- ---@class details_storage_unitscore : table
- ---@field total number
- ---@field persecond number
- ---@field storageUnitResult details_storage_unitresult?
- ---@field encounterKillInfo details_encounterkillinfo?
- ---@field unitName actorname?
-
- ---@type table
- local unitScores = {}
-
- ---@type table
- local encountersTable = savedData[difficulty]
- if (encountersTable) then
- local allEncountersStored = encountersTable[encounterId]
- if (allEncountersStored) then
- for index, encounterKillInfo in ipairs(allEncountersStored) do
- if (encounterKillInfo.guild == guildName) then
- local roleTable = encounterKillInfo[role]
- for thisUnitName, storageUnitResult in pairs(roleTable) do
- ---@cast storageUnitResult details_storage_unitresult
- if (not unitScores[thisUnitName]) then
- unitScores[thisUnitName] = {
- total = 0,
- persecond = 0,
- unitName = thisUnitName,
- }
- end
-
- --in this part the code is searching what is the performance of each unit in
- --all encounters saved for the guild in the specific difficulty and role
-
- local total = storageUnitResult.total
- local persecond = total / encounterKillInfo.elapsed
-
- if (dps) then
- if (persecond > unitScores[thisUnitName].persecond) then
- unitScores[thisUnitName].total = total
- unitScores[thisUnitName].persecond = total / encounterKillInfo.elapsed
- unitScores[thisUnitName].storageUnitResult = storageUnitResult
- unitScores[thisUnitName].encounterKillInfo = encounterKillInfo
- end
- else
- if (total > unitScores[thisUnitName].total) then
- unitScores[thisUnitName].total = total
- unitScores[thisUnitName].persecond = total / encounterKillInfo.elapsed
- unitScores[thisUnitName].storageUnitResult = storageUnitResult
- unitScores[thisUnitName].encounterKillInfo = encounterKillInfo
- end
- end
- end
- end
- end
-
- --if the unit requested in the function parameter is not in the unitScores table, return
- if (not unitScores[unitName]) then
- return
- end
-
- local sortedResults = {}
- for playerName, playerTable in pairs(unitScores) do
- playerTable[1] = playerTable.total
- playerTable[2] = playerTable.persecond
- tinsert(sortedResults, playerTable)
- end
-
- table.sort(sortedResults, dps and Details.Sort2 or Details.Sort1)
-
- for positionIndex = 1, #sortedResults do
- if (sortedResults[positionIndex].unitName == unitName) then
- local result = {positionIndex, sortedResults[positionIndex].storageUnitResult, sortedResults[positionIndex].encounterKillInfo}
- Details:Destroy(unitScores)
- Details:Destroy(sortedResults)
- return unpack(result)
- end
- end
- end
- end
-end
-
-
----find and return the best result from a specific unit in a specific encounter and difficulty
----@param difficulty string
----@param encounterId number
----@param role role
----@param unitName actorname
----@param dps boolean?
----@return details_storage_unitresult storageUnitResult?
----@return details_encounterkillinfo encounterKillInfo?
-function Details222.storage.GetBestFromPlayer(difficulty, encounterId, role, unitName, dps)
- ---@type details_storage?
- local savedData = Details222.storage.OpenRaidStorage()
-
- if (not savedData) then
- return
- end
-
- ---@type details_storage_unitresult
- local bestStorageUnitResult
- ---@type details_encounterkillinfo
- local bestEncounterKillInfo
- local topPerSecond
-
- if (not role) then
- role = "DAMAGER"
- end
-
- local encountersTable = savedData[difficulty]
- if (encountersTable) then
- local allEncountersStored = encountersTable[encounterId]
- if (allEncountersStored) then
- for index, encounterKillInfo in ipairs(allEncountersStored) do
- local storageUnitResult = encounterKillInfo[role] and encounterKillInfo[role] [unitName]
- if (storageUnitResult) then
- if (bestStorageUnitResult) then
- if (dps) then
- if (storageUnitResult.total/encounterKillInfo.elapsed > topPerSecond) then
- bestEncounterKillInfo = encounterKillInfo
- bestStorageUnitResult = storageUnitResult
- topPerSecond = storageUnitResult.total/encounterKillInfo.elapsed
- end
- else
- if (storageUnitResult.total > bestStorageUnitResult.total) then
- bestEncounterKillInfo = encounterKillInfo
- bestStorageUnitResult = storageUnitResult
- end
- end
- else
- bestEncounterKillInfo = encounterKillInfo
- bestStorageUnitResult = storageUnitResult
- topPerSecond = storageUnitResult.total/encounterKillInfo.elapsed
- end
- end
- end
- end
- end
-
- return bestStorageUnitResult, bestEncounterKillInfo
-end
-
---network
-function Details222.storage.DBGuildSync()
- Details:SendGuildData("GS", "R")
-end
-
-local hasEncounterByEncounterSyncId = function(savedData, encounterSyncId)
- local minTime = encounterSyncId - 120
- local maxTime = encounterSyncId + 120
-
- for difficultyId, encounterIdTable in pairs(savedData or {}) do
- if (type(encounterIdTable) == "table") then
- for dungeonEncounterID, encounterTable in pairs(encounterIdTable) do
- for index, encounter in ipairs(encounterTable) do
- --check if the encounter fits in the timespam window
- if (encounter.time >= minTime and encounter.time <= maxTime) then
- return true
- end
- if (encounter.servertime) then
- if (encounter.servertime >= minTime and encounter.servertime <= maxTime) then
- return true
- end
- end
- end
- end
- end
- end
- return false
-end
-
-local recentRequestedIDs = {}
-local hasRecentRequestedEncounterSyncId = function(encounterSyncId)
- local minTime = encounterSyncId - 120
- local maxTime = encounterSyncId + 120
-
- for requestedID in pairs(recentRequestedIDs) do
- if (requestedID >= minTime and requestedID <= maxTime) then
- return true
- end
- end
-end
-
-local allowedBossesCached = nil
-local getBossIdsForCurrentExpansion = function() --need to check this!
- if (allowedBossesCached) then
- return allowedBossesCached
- end
-
- --make a list of raids and bosses that belong to the current expansion
- local _, bossInfoTable = Details:GetExpansionBossList()
- local allowedBosses = {}
-
- for bossId, bossTable in pairs(bossInfoTable) do
- ---@cast bossTable details_bossinfo
- allowedBosses[bossTable.dungeonEncounterID] = true
- allowedBosses[bossTable.journalEncounterID] = true
- allowedBosses[bossId] = true
- end
-
- allowedBossesCached = allowedBosses
- return allowedBosses
-end
-
-function Details:IsBossIdFromCurrentExpansion(bossId)
- local allowedBosses = getBossIdsForCurrentExpansion()
- return allowedBosses[bossId]
-end
-
-local currentExpZoneIds = nil
-function Details:IsZoneIdFromCurrentExpansion(zoneId)
- if (currentExpZoneIds) then
- return currentExpZoneIds[zoneId]
- end
-
- currentExpZoneIds = {}
-
- local _, bossInfoTable, raidInfoTable = Details:GetExpansionBossList()
- for bossId, bossTable in pairs(bossInfoTable) do
- ---@cast bossTable details_bossinfo
- if (bossTable.uiMapId) then
- currentExpZoneIds[bossTable.uiMapId] = true
- currentExpZoneIds[bossTable.instanceId] = true
- currentExpZoneIds[bossTable.journalInstanceId] = true
- end
- end
-
- for raidInstanceID, raidTable in pairs(raidInfoTable) do
- currentExpZoneIds[raidInstanceID] = true
- currentExpZoneIds[raidTable.raidMapID] = true
- end
-
- return currentExpZoneIds[zoneId]
-end
-
----remote call RoS
----get the server time of each encounter defeated by the guild
----@return servertime[]
-function Details222.storage.GetIDsToGuildSync()
- ---@type details_storage?
- local savedData = Details222.storage.OpenRaidStorage()
-
- if (not savedData) then
- return {}
- end
-
- local myGuildName = GetGuildInfo("player")
- if (not myGuildName) then
- return {}
- end
- --myGuildName = "Patifaria"
-
- ---@type servertime[]
- local encounterSyncIds = {}
- local allowedBosses = getBossIdsForCurrentExpansion()
-
- --build the encounter synchronized ID list
- for i, diffName in ipairs(Details222.storage.DiffNames) do
- ---@type table
- local encountersTable = savedData[diffName]
-
- for dungeonEncounterID, allEncountersStored in pairs(encountersTable) do
- if (allowedBosses[dungeonEncounterID]) then
- for index, encounterKillInfo in ipairs(allEncountersStored) do
- if (encounterKillInfo.servertime) then
- if (myGuildName == encounterKillInfo.guild) then
- tinsert(encounterSyncIds, encounterKillInfo.servertime)
- end
- end
- end
- end
- end
- end
-
- if (Details.debug) then
- Details:Msg("(debug) [RoS-EncounterSync] sending " .. #encounterSyncIds .. " IDs.")
- end
-
- return encounterSyncIds
-end
-
---local call RoC - received the encounterSyncIds - need to know which fights is missing
----@param encounterSyncIds servertime[]
-function Details222.storage.CheckMissingIDsToGuildSync(encounterSyncIds)
- ---@type details_storage?
- local savedData = Details222.storage.OpenRaidStorage()
-
- if (not savedData) then
- return
- end
-
- if (type(encounterSyncIds) ~= "table") then
- if (Details.debug) then
- Details:Msg("(debug) [RoS-EncounterSync] RoC encounterSyncIds isn't a table.")
- end
- return
- end
-
- --store the IDs which need to be sync
- local requestEncounterSyncIds = {}
-
- --check missing IDs
- for index, encounterSyncId in ipairs(encounterSyncIds) do
- if (not hasEncounterByEncounterSyncId(savedData, encounterSyncId)) then
- if (not hasRecentRequestedEncounterSyncId(encounterSyncId)) then
- tinsert(requestEncounterSyncIds, encounterSyncId)
- recentRequestedIDs[encounterSyncId] = true
- end
- end
- end
-
- if (Details.debug) then
- Details:Msg("(debug) [RoC-EncounterSync] RoS found " .. #requestEncounterSyncIds .. " encounters out dated.")
- end
-
- return requestEncounterSyncIds
-end
-
---remote call RoS - build the encounter list from the encounterSyncIds
----@param encounterSyncIds servertime[]
-function Details222.storage.BuildEncounterDataToGuildSync(encounterSyncIds)
- ---@type details_storage?
- local savedData = Details222.storage.OpenRaidStorage()
-
- if (not savedData) then
- return
- end
-
- if (type(encounterSyncIds) ~= "table") then
- if (Details.debug) then
- Details:Msg("(debug) [RoS-EncounterSync] IDsList isn't a table.")
- end
- return
- end
-
- local amtToSend = 0
- local maxAmount = 0
-
- ---@type table>[]
- local encounterList = {}
-
- ---@type table>
- local currentTable = {}
-
- tinsert(encounterList, currentTable)
-
- if (Details.debug) then
- Details:Msg("(debug) [RoS-EncounterSync] the client requested " .. #encounterSyncIds .. " encounters.")
- end
-
- for index, encounterSyncId in ipairs(encounterSyncIds) do
- for difficulty, encountersTable in pairs(savedData) do
- ---@cast encountersTable details_encounterkillinfo[]
- if (Details222.storage.DiffNamesHash[difficulty]) then --this ensures that the difficulty is valid
- for dungeonEncounterID, allEncountersStored in pairs(encountersTable) do
- for index, encounterKillInfo in ipairs(allEncountersStored) do
- ---@cast encounterKillInfo details_encounterkillinfo
- if (encounterSyncId == encounterKillInfo.time or encounterSyncId == encounterKillInfo.servertime) then --the time here is always exactly
- --send this encounter
- currentTable[difficulty] = currentTable[difficulty] or {}
- currentTable[difficulty][dungeonEncounterID] = currentTable[difficulty][dungeonEncounterID] or {}
-
- tinsert(currentTable[difficulty][dungeonEncounterID], encounterKillInfo)
-
- amtToSend = amtToSend + 1
- maxAmount = maxAmount + 1
-
- if (maxAmount == 3) then
- currentTable = {}
- tinsert(encounterList, currentTable)
- maxAmount = 0
- end
- end
- end
- end
- end
- end
- end
-
- if (Details.debug) then
- Details:Msg("(debug) [RoS-EncounterSync] sending " .. amtToSend .. " encounters.")
- end
-
- --the resulting table is a table with subtables, each subtable has a maximum of 3 encounters on indexes 1, 2 and 3
- --resulting in
- --{
- -- {[raid_difficulty_eng_name_lowercase][encounterid] = {details_encounterkillinfo, details_encounterkillinfo, details_encounterkillinfo}},
- -- {[raid_difficulty_eng_name_lowercase][encounterid] = {details_encounterkillinfo, details_encounterkillinfo, details_encounterkillinfo}}
- --}
- return encounterList
-end
-
-
---local call RoC - add the fights to the client db
-function Details222.storage.AddGuildSyncData(data, source)
- ---@type details_storage?
- local savedData = Details222.storage.OpenRaidStorage()
-
- if (not savedData) then
- return
- end
-
- if (not data or type(data) ~= "table") then
- if (Details.debug) then
- Details:Msg("(debug) [RoC-AddGuildSyncData] data isn't a table.")
- end
- return
- end
-
- local addedAmount = 0
- Details.LastGuildSyncReceived = GetTime()
- local allowedBosses = getBossIdsForCurrentExpansion()
-
- ---@cast data raid_difficulty_eng_name_lowercase, table
-
- for difficulty, encounterIdTable in pairs(data) do
- ---@cast encounterIdTable table
-
- if (Details222.storage.DiffNamesHash[difficulty] and type(encounterIdTable) == "table") then
- for dungeonEncounterID, allEncountersStored in pairs(encounterIdTable) do
- if (type(dungeonEncounterID) == "number" and type(allEncountersStored) == "table" and allowedBosses[dungeonEncounterID]) then
- for index, encounterKillInfo in ipairs(allEncountersStored) do
- --validate the encounter
- if (type(encounterKillInfo.servertime) == "number" and type(encounterKillInfo.time) == "number" and type(encounterKillInfo.guild) == "string" and type(encounterKillInfo.date) == "string" and type(encounterKillInfo.HEALER) == "table" and type(encounterKillInfo.elapsed) == "number" and type(encounterKillInfo.DAMAGER) == "table") then
- --check if this encounter already has been added from another sync
- if (not hasEncounterByEncounterSyncId(savedData, encounterKillInfo.servertime)) then
- savedData[difficulty] = savedData[difficulty] or {}
- savedData[difficulty][dungeonEncounterID] = savedData[difficulty][dungeonEncounterID] or {}
- tinsert(savedData[difficulty][dungeonEncounterID], encounterKillInfo)
-
- if (_G.DetailsRaidHistoryWindow and _G.DetailsRaidHistoryWindow:IsShown()) then
- _G.DetailsRaidHistoryWindow:Refresh()
- end
-
- addedAmount = addedAmount + 1
- else
- if (Details.debug) then
- Details:Msg("(debug) [RoC-AddGuildSyncData] received a duplicated encounter table.")
- end
- end
- else
- if (Details.debug) then
- Details:Msg("(debug) [RoC-AddGuildSyncData] received an invalid encounter table.")
- end
- end
- end
- end
- end
- end
- end
-
- if (Details.debug) then
- Details:Msg("(debug) [RoC-AddGuildSyncData] added " .. addedAmount .. " to database.")
- end
-
- if (_G.DetailsRaidHistoryWindow and _G.DetailsRaidHistoryWindow:IsShown()) then
- _G.DetailsRaidHistoryWindow:UpdateDropdowns()
- _G.DetailsRaidHistoryWindow:Refresh()
- end
-end
-
----@param difficulty string
----@return encounterid[]
-function Details222.storage.ListEncounters(difficulty)
- ---@type details_storage?
- local savedData = Details222.storage.OpenRaidStorage()
-
- if (not savedData) then
- return {}
- end
-
- if (not difficulty) then
- return {}
- end
-
- ---@type encounterid[]
- local resultTable = {}
-
- local encountersTable = savedData[difficulty]
- if (encountersTable) then
- for dungeonEncounterID in pairs(encountersTable) do
- tinsert(resultTable, dungeonEncounterID)
- end
- end
-
- return resultTable
-end
-
----@param difficulty string
----@param dungeonEncounterID encounterid
----@param role role
----@param unitName actorname
----@return details_storage_unitresult[]
-function Details222.storage.GetUnitData(difficulty, dungeonEncounterID, role, unitName)
- local savedData = Details222.storage.OpenRaidStorage()
-
- if (not savedData) then
- return {}
- end
-
- assert(type(unitName) == "string", "unitName must be a string.")
- assert(type(dungeonEncounterID) == "number", "dungeonEncounterID must be a string.")
-
- ---@type details_storage_unitresult[]
- local resultTable = {}
-
- ---@type details_encounterkillinfo[]
- local encountersTable = savedData[difficulty]
- if (encountersTable) then
- local allEncountersStored = encountersTable[dungeonEncounterID]
- if (allEncountersStored) then
- for i = 1, #allEncountersStored do
- ---@type details_encounterkillinfo
- local encounterKillInfo = allEncountersStored[i]
- local playerData = encounterKillInfo[role][unitName]
- if (playerData) then
- tinsert(resultTable, playerData)
- end
- end
- end
- end
-
- return resultTable
-end
-
----return a table with all encounters saved for a specific guild in a specific difficulty for a specific encounter
----@param difficulty string
----@param dungeonEncounterID encounterid
----@param guildName guildname
----@return details_encounterkillinfo[]
-function Details222.storage.GetEncounterData(difficulty, dungeonEncounterID, guildName)
- ---@type details_storage?
- local savedData = Details222.storage.OpenRaidStorage()
-
- if (not savedData) then
- return
- end
-
- local encountersTable = savedData[difficulty]
-
- assert(encountersTable, "Difficulty not found. Use: normal, heroic or mythic.")
- assert(type(dungeonEncounterID) == "number", "dungeonEncounterID must be a number.")
-
- ---@type details_encounterkillinfo[]
- local allEncountersStored = encountersTable[dungeonEncounterID]
-
- local resultTable = {}
-
- if (not allEncountersStored) then
- return resultTable
- end
-
- for i = 1, #allEncountersStored do
- local encounterKillInfo = allEncountersStored[i]
- if (encounterKillInfo.guild == guildName) then
- tinsert(resultTable, encounterKillInfo)
- end
- end
-
- return resultTable
-end
-
----load the storage addon when the player leave combat, this function is also called from the parser when the player has its regen enabled
-function Details.ScheduleLoadStorage()
- --check first if the storage is already loaded
- if (C_AddOns.IsAddOnLoaded(CONST_ADDONNAME_DATASTORAGE)) then
- Details.schedule_storage_load = nil
- Details222.storageLoaded = true
- return
- end
-
- if (InCombatLockdown() or UnitAffectingCombat("player")) then
- if (Details.debug) then
- print("|cFFFFFF00Details! storage scheduled to load (player in combat).")
- end
- --load when the player leave combat
- Details.schedule_storage_load = true
- return
- else
- if (not C_AddOns.IsAddOnLoaded(CONST_ADDONNAME_DATASTORAGE)) then
- local bSuccessLoaded, reason = C_AddOns.LoadAddOn(CONST_ADDONNAME_DATASTORAGE)
- if (not bSuccessLoaded) then
- if (Details.debug) then
- print("|cFFFFFF00Details! Storage|r: can't load storage, may be the addon is disabled.")
- end
- return
- end
- createStorageTables()
- end
- end
-
- if (C_AddOns.IsAddOnLoaded(CONST_ADDONNAME_DATASTORAGE)) then
- Details.schedule_storage_load = nil
- Details222.storageLoaded = true
- if (Details.debug) then
- print("|cFFFFFF00Details! storage loaded.")
- end
- else
- if (Details.debug) then
- print("|cFFFFFF00Details! fail to load storage, scheduled once again.")
- end
- Details.schedule_storage_load = true
- end
-end
-
-function Details.GetStorage()
- return DetailsDataStorage
-end
-
---this function is used on the breakdown window to show ranking and on the main window when hovering over the spec icon
---if the storage is not loaded, it will try to load it even if the player is in combat
-function Details.OpenStorage()
- --if the player is in combat, this function return false, if failed to load by other reason it returns nil
- --check if the storage is already loaded
- if (not C_AddOns.IsAddOnLoaded(CONST_ADDONNAME_DATASTORAGE)) then
- --can't open it during combat
- if (InCombatLockdown() or UnitAffectingCombat("player")) then
- if (Details.debug) then
- print("|cFFFFFF00Details! Storage|r: can't load storage due to combat.")
- end
- return false
- end
-
- local loaded, reason = C_AddOns.LoadAddOn(CONST_ADDONNAME_DATASTORAGE)
- if (not loaded) then
- if (Details.debug) then
- print("|cFFFFFF00Details! Storage|r: can't load storage, may be the addon is disabled.")
- end
- return
- end
-
- local savedData = createStorageTables()
-
- if (savedData and C_AddOns.IsAddOnLoaded(CONST_ADDONNAME_DATASTORAGE)) then
- Details222.storageLoaded = true
- end
-
- return DetailsDataStorage
- else
- return DetailsDataStorage
- end
-end
-
-Details.Database = {}
-
---this function is called on storewipe and storeencounter
----@return details_storage?
-function Details.Database.LoadDB()
- --check if the storage is not loaded yet and try to load it
- if (not C_AddOns.IsAddOnLoaded(CONST_ADDONNAME_DATASTORAGE)) then
- local loaded, reason = C_AddOns.LoadAddOn(CONST_ADDONNAME_DATASTORAGE)
- if (not loaded) then
- if (Details.debug) then
- print("|cFFFFFF00Details! Storage|r: can't save the encounter, couldn't load DataStorage, may be the addon is disabled.")
- end
- return
- end
- end
-
- --get the storage table
- local savedData = _G.DetailsDataStorage
-
- if (not savedData and Details.CreateStorageDB) then
- savedData = Details:CreateStorageDB()
- if (not savedData) then
- if (Details.debug) then
- print("|cFFFFFF00Details! Storage|r: can't save the encounter, couldn't load DataStorage, may be the addon is disabled.")
- end
- return
- end
-
- elseif (not savedData) then
- if (Details.debug) then
- print("|cFFFFFF00Details! Storage|r: can't save the encounter, couldn't load DataStorage, may be the addon is disabled.")
- end
- return
- end
-
- return savedData
-end
-
----@param savedData details_storage
-function Details.Database.GetBossKillsDB(savedData)
- return savedData.totalkills
-end
-
----@param combat combat?
-function Details.Database.StoreWipe(combat)
- if (not combat) then
- combat = Details:GetCurrentCombat()
- end
-
- if (not combat) then
- if (Details.debug) then
- print("|cFFFFFF00Details! Storage|r: combat not found.")
- end
- return
- end
-
- local name, type, zoneDifficulty, difficultyName, maxPlayers, playerDifficulty, isDynamicInstance, mapID, instanceGroupSize = GetInstanceInfo()
-
- if (not Details:IsZoneIdFromCurrentExpansion(mapID) and not Details222.storage.IsDebug) then
- if (Details.debug) then
- print("|cFFFFFF00Details! Storage|r: instance not allowed.") --again
- end
- return
- end
-
- local bossInfo = combat:GetBossInfo()
- local dungeonEncounterID = bossInfo and bossInfo.id
-
- if (not dungeonEncounterID) then
- if (Details.debug) then
- print("|cFFFFFF00Details! Storage|r: encounter ID not found.")
- end
- return
- end
-
- --get the difficulty
- local _, difficulty = combat:GetDifficulty()
-
- --load database
- ---@type details_storage?
- local savedData = Details.Database.LoadDB()
- if (not savedData) then
- return
- end
-
- if (IsInRaid()) then
- --total kills in a boss on raid or dungeon
- local totalKillsDataBase = Details.Database.GetBossKillsDB(savedData)
-
- totalKillsDataBase[difficulty] = totalKillsDataBase[difficulty] or {}
- totalKillsDataBase[difficulty][dungeonEncounterID] = totalKillsDataBase[difficulty][dungeonEncounterID] or {
- kills = 0,
- wipes = 0,
- time_fasterkill = 0,
- time_fasterkill_when = 0,
- time_incombat = 0,
- dps_best = 0,
- dps_best_when = 0,
- dps_best_raid = 0,
- dps_best_raid_when = 0
- }
-
- local bossData = totalKillsDataBase[difficulty][dungeonEncounterID]
- bossData.wipes = bossData.wipes + 1
- end
-end
-
----@param combat combat
-function Details.Database.StoreEncounter(combat)
- --stop execution if the expansion isn't retail
- if (not detailsFramework:IsDragonflightAndBeyond()) then
- return
- end
-
- combat = combat or Details:GetCurrentCombat()
-
- if (not combat) then
- if (Details.debug) then
- print("|cFFFFFF00Details! Storage|r: combat not found.")
- end
- return
- end
-
- local name, type, difficulty, difficultyName, maxPlayers, playerDifficulty, isDynamicInstance, mapID, instanceGroupSize = GetInstanceInfo()
-
- --Details:IsZoneIdFromCurrentExpansion(select(8, GetInstanceInfo()))
-
- if (not Details:IsZoneIdFromCurrentExpansion(mapID) and not Details222.storage.IsDebug) then
- if (Details.debug) then
- print("|cFFFFFF00Details! Storage|r: instance not allowed.")
- end
- return
- end
-
- local encounterInfo = combat:GetBossInfo()
- local encounterId = encounterInfo and encounterInfo.id
-
- if (not encounterId) then
- if (Details.debug) then
- print("|cFFFFFF00Details! Storage|r: encounter ID not found.")
- end
- return
- end
-
- --get the difficulty
- local diffId, diffName = combat:GetDifficulty()
- if (Details.debug) then
- print("|cFFFFFF00Details! Storage|r: difficulty identified:", diffId, diffName)
- end
-
- --database
- ---@type details_storage?
- local savedData = Details.Database.LoadDB()
- if (not savedData) then
- if (Details.debug) then
- print("|cFFFFFF00Details! Storage|r: Details.Database.LoadDB() FAILED!")
- end
- return
- end
-
- --[=[
- savedData[mythic] = {
- [encounterId] = { --indexed table
- [1] = {
- DAMAGER = {
- [actorname] = details_storage_unitresult
- },
- HEALER = {
- [actorname] = details_storage_unitresult
- },
- date = date("%H:%M %d/%m/%y"),
- time = time(),
- servertime = GetServerTime(),
- elapsed = combat:GetCombatTime(),
- guild = guildName,
- }
- }
- }
- --]=]
-
- ---@type combattime
- local elapsedCombatTime = combat:GetCombatTime()
-
- ---@type table
- local encountersTable = savedData[diffName]
- if (not encountersTable) then
- Details:Msg("encountersTable not found, diffName:", diffName)
- savedData[diffName] = {}
- encountersTable = savedData[diffName]
- end
-
- ---@type details_encounterkillinfo[]
- local allEncountersStored = encountersTable[encounterId]
- if (not allEncountersStored) then
- encountersTable[encounterId] = {}
- allEncountersStored = encountersTable[encounterId]
- end
-
- --total kills in a boss on raid or dungeon
- local totalkillsTable = Details.Database.GetBossKillsDB(savedData)
-
- --store total kills on this boss
- --if the player is facing a raid boss
- if (IsInRaid()) then
- totalkillsTable[encounterId] = totalkillsTable[encounterId] or {}
- totalkillsTable[encounterId][diffName] = totalkillsTable[encounterId][diffName] or {
- kills = 0,
- wipes = 0,
- time_fasterkill = 1000000,
- time_fasterkill_when = 0,
- time_incombat = 0,
- dps_best = 0, --player best dps
- dps_best_when = 0, --when the player did the best dps
- dps_best_raid = 0,
- dps_best_raid_when = 0
- }
-
- ---@type details_bosskillinfo
- local bossData = totalkillsTable[encounterId][diffName]
- ---@type combattime
- local encounterElapsedTime = elapsedCombatTime
-
- --kills amount
- bossData.kills = bossData.kills + 1
-
- --best time
- if (encounterElapsedTime < bossData.time_fasterkill) then
- bossData.time_fasterkill = encounterElapsedTime
- bossData.time_fasterkill_when = time()
- end
-
- --total time in combat
- bossData.time_incombat = bossData.time_incombat + encounterElapsedTime
-
- --player best dps
- ---@actor
- local playerActorObject = combat(DETAILS_ATTRIBUTE_DAMAGE, Details.playername)
- if (playerActorObject) then
- local playerDps = playerActorObject.total / encounterElapsedTime
- if (playerDps > bossData.dps_best) then
- bossData.dps_best = playerDps
- bossData.dps_best_when = time()
- end
- end
-
- --raid best dps
- local raidTotalDamage = combat:GetTotal(DETAILS_ATTRIBUTE_DAMAGE, nil, true)
- local raidDps = raidTotalDamage / encounterElapsedTime
- if (raidDps > bossData.dps_best_raid) then
- bossData.dps_best_raid = raidDps
- bossData.dps_best_raid_when = time()
- end
- end
-
- --check for heroic and mythic
- if (Details222.storage.IsDebug or Details222.storage.DiffNamesHash[diffName]) then
- --check the guild name
- local match = 0
- local guildName = GetGuildInfo("player")
- local raidSize = GetNumGroupMembers() or 0
-
- local cachedRaidUnitIds = Details222.UnitIdCache.Raid
-
- if (not Details222.storage.IsDebug) then
- if (guildName) then
- for i = 1, raidSize do
- local gName = GetGuildInfo(cachedRaidUnitIds[i]) or ""
- if (gName == guildName) then
- match = match + 1
- end
- end
-
- if (match < raidSize * 0.75) then
- if (Details.debug) then
- print("|cFFFFFF00Details! Storage|r: can't save the encounter, need at least 75% of players be from your guild.")
- end
- return
- end
- else
- if (Details.debug) then
- print("|cFFFFFF00Details! Storage|r: player isn't in a guild.")
- end
- return
- end
- else
- guildName = "Test Guild"
- end
-
- ---@type details_encounterkillinfo
- local combatResultData = {
- DAMAGER = {},
- HEALER = {},
- date = date("%H:%M %d/%m/%y"),
- time = time(),
- servertime = GetServerTime(),
- elapsed = elapsedCombatTime,
- guild = guildName,
- }
-
- local damageContainer = combat:GetContainer(DETAILS_ATTRIBUTE_DAMAGE)
- local healingContainer = combat:GetContainer(DETAILS_ATTRIBUTE_HEAL)
-
- for i = 1, GetNumGroupMembers() do
- local role = UnitGroupRolesAssigned(cachedRaidUnitIds[i])
-
- if (UnitIsInMyGuild(cachedRaidUnitIds[i])) then
- if (role == "DAMAGER" or role == "TANK") then
- local playerName = Details:GetFullName(cachedRaidUnitIds[i])
- local _, _, class = Details:GetUnitClassFull(playerName)
-
- local damagerActor = damageContainer:GetActor(playerName)
- if (damagerActor) then
- local guid = UnitGUID(cachedRaidUnitIds[i])
-
- ---@type details_storage_unitresult
- local unitResultInfo = {
- total = floor(damagerActor.total),
- itemLevel = Details:GetItemLevelFromGuid(guid),
- classId = class or 0
- }
- combatResultData.DAMAGER[playerName] = unitResultInfo
- end
-
- elseif (role == "HEALER") then
- local playerName = Details:GetFullName(cachedRaidUnitIds[i])
- local _, _, class = Details:GetUnitClassFull(playerName)
-
- local healingActor = healingContainer:GetActor(playerName)
- if (healingActor) then
- local guid = UnitGUID(cachedRaidUnitIds[i])
-
- ---@type details_storage_unitresult
- local unitResultInfo = {
- total = floor(healingActor.total),
- itemLevel = Details:GetItemLevelFromGuid(guid),
- classId = class or 0
- }
- combatResultData.HEALER[playerName] = unitResultInfo
- end
- end
- end
- end
-
- --add the encounter data
- tinsert(allEncountersStored, combatResultData)
- if (Details.debug) then
- print("|cFFFFFF00Details! Storage|r: combat data added to encounter database.")
- end
-
- local playerRole = UnitGroupRolesAssigned("player")
- ---@type details_storage_unitresult, details_encounterkillinfo
- local bestRank, encounterKillInfo = Details222.storage.GetBestFromPlayer(diffName, encounterId, playerRole, Details.playername, true) --get dps or hps
-
- if (bestRank and encounterKillInfo) then
- local registeredBestTotal = bestRank and bestRank.total or 0
- local registeredBestPerSecond = registeredBestTotal / encounterKillInfo.elapsed
-
- local currentPerSecond = 0
- if (playerRole == "DAMAGER" or playerRole == "TANK") then
- ---@actor
- local playerActorObject = damageContainer:GetActor(Details.playername)
- if (playerActorObject) then
- currentPerSecond = playerActorObject.total / elapsedCombatTime
- end
- elseif (playerRole == "HEALER") then
- ---@actor
- local playerActorObject = healingContainer:GetActor(Details.playername)
- if (playerActorObject) then
- currentPerSecond = playerActorObject.total / elapsedCombatTime
- end
- end
-
- if (registeredBestPerSecond > currentPerSecond) then
- if (not Details.deny_score_messages) then
- print(Loc ["STRING_DETAILS1"] .. format(Loc ["STRING_SCORE_NOTBEST"], Details:ToK2(currentPerSecond), Details:ToK2(registeredBestPerSecond), encounterKillInfo.date, bestRank[2]))
- end
- else
- if (not Details.deny_score_messages) then
- print(Loc ["STRING_DETAILS1"] .. format(Loc ["STRING_SCORE_BEST"], Details:ToK2(currentPerSecond)))
- end
- end
- end
-
- local lowerInstanceId = Details:GetLowerInstanceNumber()
- if (lowerInstanceId) then
- local instanceObject = Details:GetInstance(lowerInstanceId)
- if (instanceObject) then
- if (playerRole == "TANK") then
- playerRole = "DAMAGER"
- end
-
- local raidName = GetInstanceInfo()
- local func = {Details.OpenRaidHistoryWindow, Details, raidName, encounterId, diffName, playerRole, guildName}
- local icon = {[[Interface\PvPRankBadges\PvPRank08]], 16, 16, false, 0, 1, 0, 1}
- if (not Details.deny_score_messages) then
- instanceObject:InstanceAlert(Loc ["STRING_GUILDDAMAGERANK_WINDOWALERT"], icon, Details.update_warning_timeout, func, true)
- end
- end
- end
- else
- if (Details.debug) then
- print("|cFFFFFF00Details! Storage|r: raid difficulty must be heroic or mythic.")
- end
- end
-end
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--inspect stuff
diff --git a/core/parser.lua b/core/parser.lua
index f16d401fa..b3b3c2e8e 100755
--- a/core/parser.lua
+++ b/core/parser.lua
@@ -549,6 +549,8 @@
--the damage that the warlock apply to its pet through soullink is ignored
--it is not useful for damage done or friendly fire
[SPELLID_WARLOCK_SOULLINK] = true,
+ [74040] = true, --grim batol drake
+ [457658] = true, --grim batol drake
}
--expose the ignore spells table to external scripts
@@ -5509,6 +5511,7 @@ local SPELL_POWER_PAIN = SPELL_POWER_PAIN or (PowerEnum and PowerEnum.Pain) or 1
end
if (Details:IsZoneIdFromCurrentExpansion(zoneMapID)) then
+ print("encouter is from current expansion")
Details.current_exp_raid_encounters[encounterID] = true
end
diff --git a/frames/window_breakdown/window_playerbreakdown_list.lua b/frames/window_breakdown/window_playerbreakdown_list.lua
index ccc806c81..60f5c0e73 100644
--- a/frames/window_breakdown/window_playerbreakdown_list.lua
+++ b/frames/window_breakdown/window_playerbreakdown_list.lua
@@ -183,6 +183,16 @@ local createPlayerScrollBox = function(breakdownWindowFrame, breakdownSideMenu,
local updatePlayerLine = function(self, topResult, encounterId, difficultyId) --~update
local playerSelected = lastSelectedPlayerName
+ local playerInTheLine = self.playerObject
+
+ --checking if the playerObject is still valid, it could have been removed by the garbage collector
+ if (not Details:IsValidActor(playerInTheLine)) then
+ self:SetBackdropColor(unpack(scrollbox_line_backdrop_color))
+ self.specIcon:SetTexture([[Interface\Icons\INV_Misc_QuestionMark]])
+ self.playerName:SetText("")
+ return
+ end
+
if (playerSelected == self.playerObject:Name()) then
self:SetBackdropColor(unpack(scrollbox_line_backdrop_color_selected))
self.isSelected = true
diff --git a/functions/classes.lua b/functions/classes.lua
index 949f11d49..5c974ac14 100644
--- a/functions/classes.lua
+++ b/functions/classes.lua
@@ -275,6 +275,10 @@ do
Details.GetFullName = Details.GetCLName
--end
+ function Details:IsValidActor(actor)
+ return actor and actor.classe and actor.nome and actor.flag_original and true
+ end
+
function Details:Class(actor)
return self.classe or actor and actor.classe
end
diff --git a/functions/storage.lua b/functions/storage.lua
new file mode 100644
index 000000000..54d1863e8
--- /dev/null
+++ b/functions/storage.lua
@@ -0,0 +1,1296 @@
+
+local Details = _G.Details
+local addonName, Details222 = ...
+local Loc = LibStub("AceLocale-3.0"):GetLocale( "Details" )
+---@framework
+local detailsFramework = DetailsFramework
+local _
+
+
+
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+--storage stuff ~storage
+
+---@class details_storage_unitresult : table
+---@field total number
+---@field itemLevel number
+---@field classId number
+
+---@class details_encounterkillinfo : table
+---@field guild guildname
+---@field time unixtime
+---@field date date
+---@field elapsed number
+---@field HEALER table
+---@field servertime unixtime
+---@field DAMAGER table
+
+---@class details_bosskillinfo : table
+---@field kills number
+---@field wipes number
+---@field time_fasterkill number
+---@field time_fasterkill_when unixtime
+---@field time_incombat number
+---@field dps_best number
+---@field dps_best_when unixtime
+---@field dps_best_raid number
+---@field dps_best_raid_when unixtime
+
+---@class details_storage : table
+---@field VERSION number the database version
+---@field normal table
+---@field heroic table
+---@field mythic table
+---@field mythic_plus table
+---@field saved_encounters table
+---@field totalkills table>
+
+---@class details_storage_feature : table
+---@field diffNames string[] {"normal", "heroic", "mythic", "raidfinder"}
+---@field OpenRaidStorage fun():details_storage
+---@field HaveDataForEncounter fun(difficulty:string, encounterId:number, guildName:string|boolean):boolean
+---@field GetBestFromGuild fun(difficulty:string, encounterId:number, role:role, dps:boolean, guildName:string):actorname, details_storage_unitresult, details_encounterkillinfo
+---@field GetUnitGuildRank fun(difficulty:string, encounterId:number, role:role, guildName:guildname, unitName:actorname):number?, details_storage_unitresult?, details_encounterkillinfo?
+---@field GetBestFromPlayer fun(difficulty:string, encounterId:number, role:role, dps:boolean, playerName:actorname):details_storage_unitresult, details_encounterkillinfo
+---@field DBGuildSync fun()
+
+local CONST_ADDONNAME_DATASTORAGE = "Details_DataStorage"
+
+local diffNumberToName = Details222.storage.DiffIdToName
+
+local createStorageTables = function()
+ local storageDatabase = DetailsDataStorage
+
+ if (not storageDatabase and Details.CreateStorageDB) then
+ storageDatabase = Details:CreateStorageDB()
+ if (not storageDatabase) then
+ return
+ end
+
+ elseif (not storageDatabase) then
+ return
+ end
+
+ return storageDatabase
+end
+
+---@return details_storage?
+function Details222.storage.OpenRaidStorage()
+ --check if the storage is already loaded
+ if (not C_AddOns.IsAddOnLoaded(CONST_ADDONNAME_DATASTORAGE)) then
+ local loaded, reason = C_AddOns.LoadAddOn(CONST_ADDONNAME_DATASTORAGE)
+ if (not loaded) then
+ return
+ end
+ end
+
+ --get the storage table
+ local savedData = DetailsDataStorage
+
+ if (not savedData and Details.CreateStorageDB) then
+ savedData = Details:CreateStorageDB()
+ if (not savedData) then
+ return
+ end
+
+ elseif (not savedData) then
+ return
+ end
+
+ return savedData
+end
+
+---check if there is data for a specific encounter and difficulty, if a guildName is passed, check if there is data for the guild
+---@param difficulty string
+---@param encounterId number
+---@param guildName string|boolean
+---@return boolean bHasData
+function Details222.storage.HaveDataForEncounter(difficulty, encounterId, guildName)
+ ---@type details_storage?
+ local savedData = Details222.storage.OpenRaidStorage()
+ if (not savedData) then
+ return false
+ end
+
+ difficulty = diffNumberToName[difficulty] or difficulty
+
+ if (guildName and type(guildName) == "boolean") then
+ guildName = GetGuildInfo("player")
+ end
+
+ ---@type table
+ local encountersTable = savedData[difficulty]
+ if (encountersTable) then
+ local allEncountersStored = encountersTable[encounterId]
+ if (allEncountersStored) then
+ --didn't requested a guild name, so just return 'we have data for this encounter'
+ if (not guildName) then
+ return true
+ end
+
+ --data for a specific guild is requested, check if there is data for the guild
+ for index, encounterKillInfo in ipairs(allEncountersStored) do
+ if (encounterKillInfo.guild == guildName) then
+ return true
+ end
+ end
+ end
+ end
+
+ return false
+end
+
+---find the best unit from a specific role from a specific guild in a specific encounter and difficulty
+---check all encounters saved for the guild and difficulty and return the unit with the best performance
+---@param difficulty string
+---@param encounterId number
+---@param role role
+---@param dps boolean?
+---@param guildName string
+---@return boolean|string playerName
+---@return boolean|details_storage_unitresult storageUnitResult
+---@return boolean|details_encounterkillinfo encounterKillInfo
+function Details222.storage.GetBestFromGuild(difficulty, encounterId, role, dps, guildName)
+ ---@type details_storage?
+ local savedData = Details222.storage.OpenRaidStorage()
+
+ if (not savedData) then
+ return false, false, false
+ end
+
+ if (not guildName) then
+ guildName = GetGuildInfo("player")
+ end
+
+ if (not guildName) then
+ if (Details.debug) then
+ Details:Msg("(debug) GetBestFromGuild() guild name invalid.")
+ end
+ return false, false, false
+ end
+
+ local best = 0
+ local bestDps = 0
+ local bestEncounterKillInfo
+ local bestUnitName
+ local bestStorageResultTable
+
+ if (not role) then
+ role = "DAMAGER"
+ end
+
+ ---@type table
+ local encountersTable = savedData[difficulty]
+ if (encountersTable) then
+ local allEncountersStored = encountersTable[encounterId]
+ if (allEncountersStored) then
+ for index, encounterKillInfo in ipairs(allEncountersStored) do
+ if (encounterKillInfo.guild == guildName) then
+ ---@type table
+ local unitListFromRole = encounterKillInfo[role]
+ if (unitListFromRole) then
+ for unitName, storageUnitResult in pairs(unitListFromRole) do
+ if (dps) then
+ if (storageUnitResult.total / encounterKillInfo.elapsed > bestDps) then
+ bestDps = storageUnitResult.total / encounterKillInfo.elapsed
+ bestUnitName = unitName
+ bestEncounterKillInfo = encounterKillInfo
+ bestStorageResultTable = storageUnitResult
+ end
+ else
+ if (storageUnitResult.total > best) then
+ best = storageUnitResult.total
+ bestUnitName = unitName
+ bestEncounterKillInfo = encounterKillInfo
+ bestStorageResultTable = storageUnitResult
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ return bestUnitName, bestStorageResultTable, bestEncounterKillInfo
+end
+
+---find and return the rank position of a unit among all other players guild
+---the rank is based on the biggest total amount of damage or healing (role) done in a specific encounter and difficulty
+---@param difficulty string
+---@param encounterId number
+---@param role role
+---@param unitName actorname
+---@param dps boolean?
+---@param guildName guildname
+---@return number? positionIndex
+---@return details_storage_unitresult? storageUnitResult
+---@return details_encounterkillinfo? encounterKillInfo
+function Details222.storage.GetUnitGuildRank(difficulty, encounterId, role, unitName, dps, guildName)
+ ---@type details_storage?
+ local savedData = Details222.storage.OpenRaidStorage()
+
+ if (not savedData) then
+ return
+ end
+
+ if (not guildName) then
+ guildName = GetGuildInfo("player")
+ end
+
+ if (not guildName) then
+ if (Details.debug) then
+ Details:Msg("(debug) GetBestFromGuild() guild name invalid.")
+ end
+ return
+ end
+
+ if (not role) then
+ role = "DAMAGER"
+ end
+
+ ---@class details_storage_unitscore : table
+ ---@field total number
+ ---@field persecond number
+ ---@field storageUnitResult details_storage_unitresult?
+ ---@field encounterKillInfo details_encounterkillinfo?
+ ---@field unitName actorname?
+
+ ---@type table
+ local unitScores = {}
+
+ ---@type table
+ local encountersTable = savedData[difficulty]
+ if (encountersTable) then
+ local allEncountersStored = encountersTable[encounterId]
+ if (allEncountersStored) then
+ for index, encounterKillInfo in ipairs(allEncountersStored) do
+ if (encounterKillInfo.guild == guildName) then
+ local roleTable = encounterKillInfo[role]
+ for thisUnitName, storageUnitResult in pairs(roleTable) do
+ ---@cast storageUnitResult details_storage_unitresult
+ if (not unitScores[thisUnitName]) then
+ unitScores[thisUnitName] = {
+ total = 0,
+ persecond = 0,
+ unitName = thisUnitName,
+ }
+ end
+
+ --in this part the code is searching what is the performance of each unit in
+ --all encounters saved for the guild in the specific difficulty and role
+
+ local total = storageUnitResult.total
+ local persecond = total / encounterKillInfo.elapsed
+
+ if (dps) then
+ if (persecond > unitScores[thisUnitName].persecond) then
+ unitScores[thisUnitName].total = total
+ unitScores[thisUnitName].persecond = total / encounterKillInfo.elapsed
+ unitScores[thisUnitName].storageUnitResult = storageUnitResult
+ unitScores[thisUnitName].encounterKillInfo = encounterKillInfo
+ end
+ else
+ if (total > unitScores[thisUnitName].total) then
+ unitScores[thisUnitName].total = total
+ unitScores[thisUnitName].persecond = total / encounterKillInfo.elapsed
+ unitScores[thisUnitName].storageUnitResult = storageUnitResult
+ unitScores[thisUnitName].encounterKillInfo = encounterKillInfo
+ end
+ end
+ end
+ end
+ end
+
+ --if the unit requested in the function parameter is not in the unitScores table, return
+ if (not unitScores[unitName]) then
+ return
+ end
+
+ local sortedResults = {}
+ for playerName, playerTable in pairs(unitScores) do
+ playerTable[1] = playerTable.total
+ playerTable[2] = playerTable.persecond
+ tinsert(sortedResults, playerTable)
+ end
+
+ table.sort(sortedResults, dps and Details.Sort2 or Details.Sort1)
+
+ for positionIndex = 1, #sortedResults do
+ if (sortedResults[positionIndex].unitName == unitName) then
+ local result = {positionIndex, sortedResults[positionIndex].storageUnitResult, sortedResults[positionIndex].encounterKillInfo}
+ Details:Destroy(unitScores)
+ Details:Destroy(sortedResults)
+ return unpack(result)
+ end
+ end
+ end
+ end
+ return nil, nil, nil
+end
+
+
+---find and return the best result from a specific unit in a specific encounter and difficulty
+---@param difficulty string
+---@param encounterId number
+---@param role role
+---@param unitName actorname
+---@param dps boolean?
+---@return details_storage_unitresult? storageUnitResult
+---@return details_encounterkillinfo? encounterKillInfo
+function Details222.storage.GetBestFromPlayer(difficulty, encounterId, role, unitName, dps)
+ ---@type details_storage?
+ local savedData = Details222.storage.OpenRaidStorage()
+
+ if (not savedData) then
+ return
+ end
+
+ ---@type details_storage_unitresult
+ local bestStorageUnitResult
+ ---@type details_encounterkillinfo
+ local bestEncounterKillInfo
+ local topPerSecond
+
+ if (not role) then
+ role = "DAMAGER"
+ end
+
+ local encountersTable = savedData[difficulty]
+ if (encountersTable) then
+ local allEncountersStored = encountersTable[encounterId]
+ if (allEncountersStored) then
+ for index, encounterKillInfo in ipairs(allEncountersStored) do
+ local storageUnitResult = encounterKillInfo[role] and encounterKillInfo[role] [unitName]
+ if (storageUnitResult) then
+ if (bestStorageUnitResult) then
+ if (dps) then
+ if (storageUnitResult.total/encounterKillInfo.elapsed > topPerSecond) then
+ bestEncounterKillInfo = encounterKillInfo
+ bestStorageUnitResult = storageUnitResult
+ topPerSecond = storageUnitResult.total/encounterKillInfo.elapsed
+ end
+ else
+ if (storageUnitResult.total > bestStorageUnitResult.total) then
+ bestEncounterKillInfo = encounterKillInfo
+ bestStorageUnitResult = storageUnitResult
+ end
+ end
+ else
+ bestEncounterKillInfo = encounterKillInfo
+ bestStorageUnitResult = storageUnitResult
+ topPerSecond = storageUnitResult.total/encounterKillInfo.elapsed
+ end
+ end
+ end
+ end
+ end
+
+ return bestStorageUnitResult, bestEncounterKillInfo
+end
+
+--network
+function Details222.storage.DBGuildSync()
+ Details:SendGuildData("GS", "R")
+end
+
+local hasEncounterByEncounterSyncId = function(savedData, encounterSyncId)
+ local minTime = encounterSyncId - 120
+ local maxTime = encounterSyncId + 120
+
+ for difficultyId, encounterIdTable in pairs(savedData or {}) do
+ if (type(encounterIdTable) == "table") then
+ for dungeonEncounterID, encounterTable in pairs(encounterIdTable) do
+ for index, encounter in ipairs(encounterTable) do
+ --check if the encounter fits in the timespam window
+ if (encounter.time >= minTime and encounter.time <= maxTime) then
+ return true
+ end
+ if (encounter.servertime) then
+ if (encounter.servertime >= minTime and encounter.servertime <= maxTime) then
+ return true
+ end
+ end
+ end
+ end
+ end
+ end
+ return false
+end
+
+local recentRequestedIDs = {}
+local hasRecentRequestedEncounterSyncId = function(encounterSyncId)
+ local minTime = encounterSyncId - 120
+ local maxTime = encounterSyncId + 120
+
+ for requestedID in pairs(recentRequestedIDs) do
+ if (requestedID >= minTime and requestedID <= maxTime) then
+ return true
+ end
+ end
+end
+
+local allowedBossesCached = nil
+local getBossIdsForCurrentExpansion = function() --need to check this!
+ if (allowedBossesCached) then
+ return allowedBossesCached
+ end
+
+ --make a list of raids and bosses that belong to the current expansion
+ local _, bossInfoTable = Details:GetExpansionBossList()
+ local allowedBosses = {}
+
+ for bossId, bossTable in pairs(bossInfoTable) do
+ ---@cast bossTable details_bossinfo
+ allowedBosses[bossTable.dungeonEncounterID] = true
+ allowedBosses[bossTable.journalEncounterID] = true
+ allowedBosses[bossId] = true
+ end
+
+ allowedBossesCached = allowedBosses
+ return allowedBosses
+end
+
+function Details:IsBossIdFromCurrentExpansion(bossId)
+ local allowedBosses = getBossIdsForCurrentExpansion()
+ return allowedBosses[bossId]
+end
+
+local currentExpZoneIds = nil
+function Details:IsZoneIdFromCurrentExpansion(zoneId)
+ if (currentExpZoneIds) then
+ return currentExpZoneIds[zoneId]
+ end
+
+ currentExpZoneIds = {}
+
+ local _, bossInfoTable, raidInfoTable = Details:GetExpansionBossList()
+ for bossId, bossTable in pairs(bossInfoTable) do
+ ---@cast bossTable details_bossinfo
+ if (bossTable.uiMapId) then
+ currentExpZoneIds[bossTable.uiMapId] = true
+ currentExpZoneIds[bossTable.instanceId] = true
+ currentExpZoneIds[bossTable.journalInstanceId] = true
+ end
+ end
+
+ for raidInstanceID, raidTable in pairs(raidInfoTable) do
+ currentExpZoneIds[raidInstanceID] = true
+ currentExpZoneIds[raidTable.raidMapID] = true
+ end
+
+ return currentExpZoneIds[zoneId]
+end
+
+---remote call RoS
+---get the server time of each encounter defeated by the guild
+---@return servertime[]
+function Details222.storage.GetIDsToGuildSync()
+ ---@type details_storage?
+ local savedData = Details222.storage.OpenRaidStorage()
+
+ if (not savedData) then
+ return {}
+ end
+
+ local myGuildName = GetGuildInfo("player")
+ if (not myGuildName) then
+ return {}
+ end
+ --myGuildName = "Patifaria"
+
+ ---@type servertime[]
+ local encounterSyncIds = {}
+ local allowedBosses = getBossIdsForCurrentExpansion()
+
+ --build the encounter synchronized ID list
+ for i, diffName in ipairs(Details222.storage.DiffNames) do
+ ---@type table
+ local encountersTable = savedData[diffName]
+
+ for dungeonEncounterID, allEncountersStored in pairs(encountersTable) do
+ if (allowedBosses[dungeonEncounterID]) then
+ for index, encounterKillInfo in ipairs(allEncountersStored) do
+ if (encounterKillInfo.servertime) then
+ if (myGuildName == encounterKillInfo.guild) then
+ tinsert(encounterSyncIds, encounterKillInfo.servertime)
+ end
+ end
+ end
+ end
+ end
+ end
+
+ if (Details.debug) then
+ Details:Msg("(debug) [RoS-EncounterSync] sending " .. #encounterSyncIds .. " IDs.")
+ end
+
+ return encounterSyncIds
+end
+
+--local call RoC - received the encounterSyncIds - need to know which fights is missing
+---@param encounterSyncIds servertime[]
+function Details222.storage.CheckMissingIDsToGuildSync(encounterSyncIds)
+ ---@type details_storage?
+ local savedData = Details222.storage.OpenRaidStorage()
+
+ if (not savedData) then
+ return
+ end
+
+ if (type(encounterSyncIds) ~= "table") then
+ if (Details.debug) then
+ Details:Msg("(debug) [RoS-EncounterSync] RoC encounterSyncIds isn't a table.")
+ end
+ return
+ end
+
+ --store the IDs which need to be sync
+ local requestEncounterSyncIds = {}
+
+ --check missing IDs
+ for index, encounterSyncId in ipairs(encounterSyncIds) do
+ if (not hasEncounterByEncounterSyncId(savedData, encounterSyncId)) then
+ if (not hasRecentRequestedEncounterSyncId(encounterSyncId)) then
+ tinsert(requestEncounterSyncIds, encounterSyncId)
+ recentRequestedIDs[encounterSyncId] = true
+ end
+ end
+ end
+
+ if (Details.debug) then
+ Details:Msg("(debug) [RoC-EncounterSync] RoS found " .. #requestEncounterSyncIds .. " encounters out dated.")
+ end
+
+ return requestEncounterSyncIds
+end
+
+--remote call RoS - build the encounter list from the encounterSyncIds
+---@param encounterSyncIds servertime[]
+function Details222.storage.BuildEncounterDataToGuildSync(encounterSyncIds)
+ ---@type details_storage?
+ local savedData = Details222.storage.OpenRaidStorage()
+
+ if (not savedData) then
+ return
+ end
+
+ if (type(encounterSyncIds) ~= "table") then
+ if (Details.debug) then
+ Details:Msg("(debug) [RoS-EncounterSync] IDsList isn't a table.")
+ end
+ return
+ end
+
+ local amtToSend = 0
+ local maxAmount = 0
+
+ ---@type table>[]
+ local encounterList = {}
+
+ ---@type table>
+ local currentTable = {}
+
+ tinsert(encounterList, currentTable)
+
+ if (Details.debug) then
+ Details:Msg("(debug) [RoS-EncounterSync] the client requested " .. #encounterSyncIds .. " encounters.")
+ end
+
+ for index, encounterSyncId in ipairs(encounterSyncIds) do
+ for difficulty, encountersTable in pairs(savedData) do
+ ---@cast encountersTable details_encounterkillinfo[]
+ if (Details222.storage.DiffNamesHash[difficulty]) then --this ensures that the difficulty is valid
+ for dungeonEncounterID, allEncountersStored in pairs(encountersTable) do
+ for index, encounterKillInfo in ipairs(allEncountersStored) do
+ ---@cast encounterKillInfo details_encounterkillinfo
+ if (encounterSyncId == encounterKillInfo.time or encounterSyncId == encounterKillInfo.servertime) then --the time here is always exactly
+ --send this encounter
+ currentTable[difficulty] = currentTable[difficulty] or {}
+ currentTable[difficulty][dungeonEncounterID] = currentTable[difficulty][dungeonEncounterID] or {}
+
+ tinsert(currentTable[difficulty][dungeonEncounterID], encounterKillInfo)
+
+ amtToSend = amtToSend + 1
+ maxAmount = maxAmount + 1
+
+ if (maxAmount == 3) then
+ currentTable = {}
+ tinsert(encounterList, currentTable)
+ maxAmount = 0
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ if (Details.debug) then
+ Details:Msg("(debug) [RoS-EncounterSync] sending " .. amtToSend .. " encounters.")
+ end
+
+ --the resulting table is a table with subtables, each subtable has a maximum of 3 encounters on indexes 1, 2 and 3
+ --resulting in
+ --{
+ -- {[raid_difficulty_eng_name_lowercase][encounterid] = {details_encounterkillinfo, details_encounterkillinfo, details_encounterkillinfo}},
+ -- {[raid_difficulty_eng_name_lowercase][encounterid] = {details_encounterkillinfo, details_encounterkillinfo, details_encounterkillinfo}}
+ --}
+ return encounterList
+end
+
+--local call RoC - add the fights to the client db
+function Details222.storage.AddGuildSyncData(data, source)
+ ---@type details_storage?
+ local savedData = Details222.storage.OpenRaidStorage()
+
+ if (not savedData) then
+ return
+ end
+
+ if (not data or type(data) ~= "table") then
+ if (Details.debug) then
+ Details:Msg("(debug) [RoC-AddGuildSyncData] data isn't a table.")
+ end
+ return
+ end
+
+ local addedAmount = 0
+ Details.LastGuildSyncReceived = GetTime()
+ local allowedBosses = getBossIdsForCurrentExpansion()
+
+ ---@cast data raid_difficulty_eng_name_lowercase, table
+
+ for difficulty, encounterIdTable in pairs(data) do
+ ---@cast encounterIdTable table
+
+ if (Details222.storage.DiffNamesHash[difficulty] and type(encounterIdTable) == "table") then
+ for dungeonEncounterID, allEncountersStored in pairs(encounterIdTable) do
+ if (type(dungeonEncounterID) == "number" and type(allEncountersStored) == "table" and allowedBosses[dungeonEncounterID]) then
+ for index, encounterKillInfo in ipairs(allEncountersStored) do
+ --validate the encounter
+ if (type(encounterKillInfo.servertime) == "number" and type(encounterKillInfo.time) == "number" and type(encounterKillInfo.guild) == "string" and type(encounterKillInfo.date) == "string" and type(encounterKillInfo.HEALER) == "table" and type(encounterKillInfo.elapsed) == "number" and type(encounterKillInfo.DAMAGER) == "table") then
+ --check if this encounter already has been added from another sync
+ if (not hasEncounterByEncounterSyncId(savedData, encounterKillInfo.servertime)) then
+ savedData[difficulty] = savedData[difficulty] or {}
+ savedData[difficulty][dungeonEncounterID] = savedData[difficulty][dungeonEncounterID] or {}
+ tinsert(savedData[difficulty][dungeonEncounterID], encounterKillInfo)
+
+ if (_G.DetailsRaidHistoryWindow and _G.DetailsRaidHistoryWindow:IsShown()) then
+ _G.DetailsRaidHistoryWindow:Refresh()
+ end
+
+ addedAmount = addedAmount + 1
+ else
+ if (Details.debug) then
+ Details:Msg("(debug) [RoC-AddGuildSyncData] received a duplicated encounter table.")
+ end
+ end
+ else
+ if (Details.debug) then
+ Details:Msg("(debug) [RoC-AddGuildSyncData] received an invalid encounter table.")
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ if (Details.debug) then
+ Details:Msg("(debug) [RoC-AddGuildSyncData] added " .. addedAmount .. " to database.")
+ end
+
+ if (_G.DetailsRaidHistoryWindow and _G.DetailsRaidHistoryWindow:IsShown()) then
+ _G.DetailsRaidHistoryWindow:UpdateDropdowns()
+ _G.DetailsRaidHistoryWindow:Refresh()
+ end
+end
+
+---@param difficulty string
+---@return encounterid[]
+function Details222.storage.ListEncounters(difficulty)
+ ---@type details_storage?
+ local savedData = Details222.storage.OpenRaidStorage()
+
+ if (not savedData) then
+ return {}
+ end
+
+ if (not difficulty) then
+ return {}
+ end
+
+ ---@type encounterid[]
+ local resultTable = {}
+
+ local encountersTable = savedData[difficulty]
+ if (encountersTable) then
+ for dungeonEncounterID in pairs(encountersTable) do
+ tinsert(resultTable, dungeonEncounterID)
+ end
+ end
+
+ return resultTable
+end
+
+---@param difficulty string
+---@param dungeonEncounterID encounterid
+---@param role role
+---@param unitName actorname
+---@return details_storage_unitresult[]
+function Details222.storage.GetUnitData(difficulty, dungeonEncounterID, role, unitName)
+ local savedData = Details222.storage.OpenRaidStorage()
+
+ if (not savedData) then
+ return {}
+ end
+
+ assert(type(unitName) == "string", "unitName must be a string.")
+ assert(type(dungeonEncounterID) == "number", "dungeonEncounterID must be a string.")
+
+ ---@type details_storage_unitresult[]
+ local resultTable = {}
+
+ ---@type details_encounterkillinfo[]
+ local encountersTable = savedData[difficulty]
+ if (encountersTable) then
+ local allEncountersStored = encountersTable[dungeonEncounterID]
+ if (allEncountersStored) then
+ for i = 1, #allEncountersStored do
+ ---@type details_encounterkillinfo
+ local encounterKillInfo = allEncountersStored[i]
+ local playerData = encounterKillInfo[role][unitName]
+ if (playerData) then
+ tinsert(resultTable, playerData)
+ end
+ end
+ end
+ end
+
+ return resultTable
+end
+
+---return a table with all encounters saved for a specific guild in a specific difficulty for a specific encounter
+---@param difficulty string
+---@param dungeonEncounterID encounterid
+---@param guildName guildname
+---@return details_encounterkillinfo[]
+function Details222.storage.GetEncounterData(difficulty, dungeonEncounterID, guildName)
+ ---@type details_storage?
+ local savedData = Details222.storage.OpenRaidStorage()
+
+ if (not savedData) then
+ return
+ end
+
+ local encountersTable = savedData[difficulty]
+
+ assert(encountersTable, "Difficulty not found. Use: normal, heroic or mythic.")
+ assert(type(dungeonEncounterID) == "number", "dungeonEncounterID must be a number.")
+
+ ---@type details_encounterkillinfo[]
+ local allEncountersStored = encountersTable[dungeonEncounterID]
+
+ local resultTable = {}
+
+ if (not allEncountersStored) then
+ return resultTable
+ end
+
+ for i = 1, #allEncountersStored do
+ local encounterKillInfo = allEncountersStored[i]
+ if (encounterKillInfo.guild == guildName) then
+ tinsert(resultTable, encounterKillInfo)
+ end
+ end
+
+ return resultTable
+end
+
+---load the storage addon when the player leave combat, this function is also called from the parser when the player has its regen enabled
+function Details.ScheduleLoadStorage()
+ --check first if the storage is already loaded
+ if (C_AddOns.IsAddOnLoaded(CONST_ADDONNAME_DATASTORAGE)) then
+ Details.schedule_storage_load = nil
+ Details222.storageLoaded = true
+ return
+ end
+
+ if (InCombatLockdown() or UnitAffectingCombat("player")) then
+ if (Details.debug) then
+ print("|cFFFFFF00Details! storage scheduled to load (player in combat).")
+ end
+ --load when the player leave combat
+ Details.schedule_storage_load = true
+ return
+ else
+ if (not C_AddOns.IsAddOnLoaded(CONST_ADDONNAME_DATASTORAGE)) then
+ local bSuccessLoaded, reason = C_AddOns.LoadAddOn(CONST_ADDONNAME_DATASTORAGE)
+ if (not bSuccessLoaded) then
+ if (Details.debug) then
+ print("|cFFFFFF00Details! Storage|r: can't load storage, may be the addon is disabled.")
+ end
+ return
+ end
+ createStorageTables()
+ end
+ end
+
+ if (C_AddOns.IsAddOnLoaded(CONST_ADDONNAME_DATASTORAGE)) then
+ Details.schedule_storage_load = nil
+ Details222.storageLoaded = true
+ if (Details.debug) then
+ print("|cFFFFFF00Details! storage loaded.")
+ end
+ else
+ if (Details.debug) then
+ print("|cFFFFFF00Details! fail to load storage, scheduled once again.")
+ end
+ Details.schedule_storage_load = true
+ end
+end
+
+function Details.GetStorage()
+ return DetailsDataStorage
+end
+
+--this function is used on the breakdown window to show ranking and on the main window when hovering over the spec icon
+--if the storage is not loaded, it will try to load it even if the player is in combat
+function Details.OpenStorage()
+ --if the player is in combat, this function return false, if failed to load by other reason it returns nil
+ --check if the storage is already loaded
+ if (not C_AddOns.IsAddOnLoaded(CONST_ADDONNAME_DATASTORAGE)) then
+ --can't open it during combat
+ if (InCombatLockdown() or UnitAffectingCombat("player")) then
+ if (Details.debug) then
+ print("|cFFFFFF00Details! Storage|r: can't load storage due to combat.")
+ end
+ return false
+ end
+
+ local loaded, reason = C_AddOns.LoadAddOn(CONST_ADDONNAME_DATASTORAGE)
+ if (not loaded) then
+ if (Details.debug) then
+ print("|cFFFFFF00Details! Storage|r: can't load storage, may be the addon is disabled.")
+ end
+ return
+ end
+
+ local savedData = createStorageTables()
+
+ if (savedData and C_AddOns.IsAddOnLoaded(CONST_ADDONNAME_DATASTORAGE)) then
+ Details222.storageLoaded = true
+ end
+
+ return DetailsDataStorage
+ else
+ return DetailsDataStorage
+ end
+end
+
+Details.Database = {}
+
+--this function is called on storewipe and storeencounter
+---@return details_storage?
+function Details.Database.LoadDB()
+ --check if the storage is not loaded yet and try to load it
+ if (not C_AddOns.IsAddOnLoaded(CONST_ADDONNAME_DATASTORAGE)) then
+ local loaded, reason = C_AddOns.LoadAddOn(CONST_ADDONNAME_DATASTORAGE)
+ if (not loaded) then
+ if (Details.debug) then
+ print("|cFFFFFF00Details! Storage|r: can't save the encounter, couldn't load DataStorage, may be the addon is disabled.")
+ end
+ return
+ end
+ end
+
+ --get the storage table
+ local savedData = _G.DetailsDataStorage
+
+ if (not savedData and Details.CreateStorageDB) then
+ savedData = Details:CreateStorageDB()
+ if (not savedData) then
+ if (Details.debug) then
+ print("|cFFFFFF00Details! Storage|r: can't save the encounter, couldn't load DataStorage, may be the addon is disabled.")
+ end
+ return
+ end
+
+ elseif (not savedData) then
+ if (Details.debug) then
+ print("|cFFFFFF00Details! Storage|r: can't save the encounter, couldn't load DataStorage, may be the addon is disabled.")
+ end
+ return
+ end
+
+ return savedData
+end
+
+---@param savedData details_storage
+function Details.Database.GetBossKillsDB(savedData)
+ return savedData.totalkills
+end
+
+---@param combat combat?
+function Details.Database.StoreWipe(combat)
+ if (not combat) then
+ combat = Details:GetCurrentCombat()
+ end
+
+ if (not combat) then
+ if (Details.debug) then
+ print("|cFFFFFF00Details! Storage|r: combat not found.")
+ end
+ return
+ end
+
+ local name, type, zoneDifficulty, difficultyName, maxPlayers, playerDifficulty, isDynamicInstance, mapID, instanceGroupSize = GetInstanceInfo()
+
+ if (not Details:IsZoneIdFromCurrentExpansion(mapID) and not Details222.storage.IsDebug) then
+ if (Details.debug) then
+ print("|cFFFFFF00Details! Storage|r: instance not allowed.") --again
+ end
+ return
+ end
+
+ local bossInfo = combat:GetBossInfo()
+ local dungeonEncounterID = bossInfo and bossInfo.id
+
+ if (not dungeonEncounterID) then
+ if (Details.debug) then
+ print("|cFFFFFF00Details! Storage|r: encounter ID not found.")
+ end
+ return
+ end
+
+ --get the difficulty
+ local _, difficulty = combat:GetDifficulty()
+
+ --load database
+ ---@type details_storage?
+ local savedData = Details.Database.LoadDB()
+ if (not savedData) then
+ return
+ end
+
+ if (IsInRaid()) then
+ --total kills in a boss on raid or dungeon
+ local totalKillsDataBase = Details.Database.GetBossKillsDB(savedData)
+
+ totalKillsDataBase[difficulty] = totalKillsDataBase[difficulty] or {}
+ totalKillsDataBase[difficulty][dungeonEncounterID] = totalKillsDataBase[difficulty][dungeonEncounterID] or {
+ kills = 0,
+ wipes = 0,
+ time_fasterkill = 0,
+ time_fasterkill_when = 0,
+ time_incombat = 0,
+ dps_best = 0,
+ dps_best_when = 0,
+ dps_best_raid = 0,
+ dps_best_raid_when = 0
+ }
+
+ local bossData = totalKillsDataBase[difficulty][dungeonEncounterID]
+ bossData.wipes = bossData.wipes + 1
+ end
+end
+
+---@param combat combat
+function Details.Database.StoreEncounter(combat)
+ --stop execution if the expansion isn't retail
+ if (not detailsFramework:IsDragonflightAndBeyond()) then
+ return
+ end
+
+ combat = combat or Details:GetCurrentCombat()
+
+ if (not combat) then
+ if (Details.debug) then
+ print("|cFFFFFF00Details! Storage|r: combat not found.")
+ end
+ return
+ end
+
+ local name, type, difficulty, difficultyName, maxPlayers, playerDifficulty, isDynamicInstance, mapID, instanceGroupSize = GetInstanceInfo()
+
+ --Details:IsZoneIdFromCurrentExpansion(select(8, GetInstanceInfo()))
+
+ if (not Details:IsZoneIdFromCurrentExpansion(mapID) and not Details222.storage.IsDebug) then
+ if (Details.debug) then
+ print("|cFFFFFF00Details! Storage|r: instance not allowed.")
+ end
+ return
+ end
+
+ local encounterInfo = combat:GetBossInfo()
+ local encounterId = encounterInfo and encounterInfo.id
+
+ if (not encounterId) then
+ if (Details.debug) then
+ print("|cFFFFFF00Details! Storage|r: encounter ID not found.")
+ end
+ return
+ end
+
+ --get the difficulty
+ local diffId, diffName = combat:GetDifficulty()
+ if (Details.debug) then
+ print("|cFFFFFF00Details! Storage|r: difficulty identified:", diffId, diffName)
+ end
+
+ --database
+ ---@type details_storage?
+ local savedData = Details.Database.LoadDB()
+ if (not savedData) then
+ if (Details.debug) then
+ print("|cFFFFFF00Details! Storage|r: Details.Database.LoadDB() FAILED!")
+ end
+ return
+ end
+
+ --[=[
+ savedData[mythic] = {
+ [encounterId] = { --indexed table
+ [1] = {
+ DAMAGER = {
+ [actorname] = details_storage_unitresult
+ },
+ HEALER = {
+ [actorname] = details_storage_unitresult
+ },
+ date = date("%H:%M %d/%m/%y"),
+ time = time(),
+ servertime = GetServerTime(),
+ elapsed = combat:GetCombatTime(),
+ guild = guildName,
+ }
+ }
+ }
+ --]=]
+
+ ---@type combattime
+ local elapsedCombatTime = combat:GetCombatTime()
+
+ ---@type table
+ local encountersTable = savedData[diffName]
+ if (not encountersTable) then
+ Details:Msg("encountersTable not found, diffName:", diffName)
+ savedData[diffName] = {}
+ encountersTable = savedData[diffName]
+ end
+
+ ---@type details_encounterkillinfo[]
+ local allEncountersStored = encountersTable[encounterId]
+ if (not allEncountersStored) then
+ encountersTable[encounterId] = {}
+ allEncountersStored = encountersTable[encounterId]
+ end
+
+ --total kills in a boss on raid or dungeon
+ local totalkillsTable = Details.Database.GetBossKillsDB(savedData)
+
+ --store total kills on this boss
+ --if the player is facing a raid boss
+ if (IsInRaid()) then
+ totalkillsTable[encounterId] = totalkillsTable[encounterId] or {}
+ totalkillsTable[encounterId][diffName] = totalkillsTable[encounterId][diffName] or {
+ kills = 0,
+ wipes = 0,
+ time_fasterkill = 1000000,
+ time_fasterkill_when = 0,
+ time_incombat = 0,
+ dps_best = 0, --player best dps
+ dps_best_when = 0, --when the player did the best dps
+ dps_best_raid = 0,
+ dps_best_raid_when = 0
+ }
+
+ ---@type details_bosskillinfo
+ local bossData = totalkillsTable[encounterId][diffName]
+ ---@type combattime
+ local encounterElapsedTime = elapsedCombatTime
+
+ --kills amount
+ bossData.kills = bossData.kills + 1
+
+ --best time
+ if (encounterElapsedTime < bossData.time_fasterkill) then
+ bossData.time_fasterkill = encounterElapsedTime
+ bossData.time_fasterkill_when = time()
+ end
+
+ --total time in combat
+ bossData.time_incombat = bossData.time_incombat + encounterElapsedTime
+
+ --player best dps
+ ---@actor
+ local playerActorObject = combat(DETAILS_ATTRIBUTE_DAMAGE, Details.playername)
+ if (playerActorObject) then
+ local playerDps = playerActorObject.total / encounterElapsedTime
+ if (playerDps > bossData.dps_best) then
+ bossData.dps_best = playerDps
+ bossData.dps_best_when = time()
+ end
+ end
+
+ --raid best dps
+ local raidTotalDamage = combat:GetTotal(DETAILS_ATTRIBUTE_DAMAGE, nil, true)
+ local raidDps = raidTotalDamage / encounterElapsedTime
+ if (raidDps > bossData.dps_best_raid) then
+ bossData.dps_best_raid = raidDps
+ bossData.dps_best_raid_when = time()
+ end
+ end
+
+ --check for heroic and mythic
+ if (Details222.storage.IsDebug or Details222.storage.DiffNamesHash[diffName]) then
+ --check the guild name
+ local match = 0
+ local guildName = GetGuildInfo("player")
+ local raidSize = GetNumGroupMembers() or 0
+
+ local cachedRaidUnitIds = Details222.UnitIdCache.Raid
+
+ if (not Details222.storage.IsDebug) then
+ if (guildName) then
+ for i = 1, raidSize do
+ local gName = GetGuildInfo(cachedRaidUnitIds[i]) or ""
+ if (gName == guildName) then
+ match = match + 1
+ end
+ end
+
+ if (match < raidSize * 0.75) then
+ if (Details.debug) then
+ print("|cFFFFFF00Details! Storage|r: can't save the encounter, need at least 75% of players be from your guild.")
+ end
+ return
+ end
+ else
+ if (Details.debug) then
+ print("|cFFFFFF00Details! Storage|r: player isn't in a guild.")
+ end
+ return
+ end
+ else
+ guildName = "Test Guild"
+ end
+
+ ---@type details_encounterkillinfo
+ local combatResultData = {
+ DAMAGER = {},
+ HEALER = {},
+ date = date("%H:%M %d/%m/%y"),
+ time = time(),
+ servertime = GetServerTime(),
+ elapsed = elapsedCombatTime,
+ guild = guildName,
+ }
+
+ local damageContainer = combat:GetContainer(DETAILS_ATTRIBUTE_DAMAGE)
+ local healingContainer = combat:GetContainer(DETAILS_ATTRIBUTE_HEAL)
+
+ for i = 1, GetNumGroupMembers() do
+ local role = UnitGroupRolesAssigned(cachedRaidUnitIds[i])
+
+ if (UnitIsInMyGuild(cachedRaidUnitIds[i])) then
+ if (role == "DAMAGER" or role == "TANK") then
+ local playerName = Details:GetFullName(cachedRaidUnitIds[i])
+ local _, _, class = Details:GetUnitClassFull(playerName)
+
+ local damagerActor = damageContainer:GetActor(playerName)
+ if (damagerActor) then
+ local guid = UnitGUID(cachedRaidUnitIds[i])
+
+ ---@type details_storage_unitresult
+ local unitResultInfo = {
+ total = floor(damagerActor.total),
+ itemLevel = Details:GetItemLevelFromGuid(guid),
+ classId = class or 0
+ }
+ combatResultData.DAMAGER[playerName] = unitResultInfo
+ end
+
+ elseif (role == "HEALER") then
+ local playerName = Details:GetFullName(cachedRaidUnitIds[i])
+ local _, _, class = Details:GetUnitClassFull(playerName)
+
+ local healingActor = healingContainer:GetActor(playerName)
+ if (healingActor) then
+ local guid = UnitGUID(cachedRaidUnitIds[i])
+
+ ---@type details_storage_unitresult
+ local unitResultInfo = {
+ total = floor(healingActor.total),
+ itemLevel = Details:GetItemLevelFromGuid(guid),
+ classId = class or 0
+ }
+ combatResultData.HEALER[playerName] = unitResultInfo
+ end
+ end
+ end
+ end
+
+ --add the encounter data
+ tinsert(allEncountersStored, combatResultData)
+ if (Details.debug) then
+ print("|cFFFFFF00Details! Storage|r: combat data added to encounter database.")
+ end
+
+ local playerRole = UnitGroupRolesAssigned("player")
+ ---@type details_storage_unitresult, details_encounterkillinfo
+ local bestRank, encounterKillInfo = Details222.storage.GetBestFromPlayer(diffName, encounterId, playerRole, Details.playername, true) --get dps or hps
+
+ if (bestRank and encounterKillInfo) then
+ local registeredBestTotal = bestRank and bestRank.total or 0
+ local registeredBestPerSecond = registeredBestTotal / encounterKillInfo.elapsed
+
+ local currentPerSecond = 0
+ if (playerRole == "DAMAGER" or playerRole == "TANK") then
+ ---@actor
+ local playerActorObject = damageContainer:GetActor(Details.playername)
+ if (playerActorObject) then
+ currentPerSecond = playerActorObject.total / elapsedCombatTime
+ end
+ elseif (playerRole == "HEALER") then
+ ---@actor
+ local playerActorObject = healingContainer:GetActor(Details.playername)
+ if (playerActorObject) then
+ currentPerSecond = playerActorObject.total / elapsedCombatTime
+ end
+ end
+
+ if (registeredBestPerSecond > currentPerSecond) then
+ if (not Details.deny_score_messages) then
+ print(Loc ["STRING_DETAILS1"] .. format(Loc ["STRING_SCORE_NOTBEST"], Details:ToK2(currentPerSecond), Details:ToK2(registeredBestPerSecond), encounterKillInfo.date, bestRank[2]))
+ end
+ else
+ if (not Details.deny_score_messages) then
+ print(Loc ["STRING_DETAILS1"] .. format(Loc ["STRING_SCORE_BEST"], Details:ToK2(currentPerSecond)))
+ end
+ end
+ end
+
+ local lowerInstanceId = Details:GetLowerInstanceNumber()
+ if (lowerInstanceId) then
+ local instanceObject = Details:GetInstance(lowerInstanceId)
+ if (instanceObject) then
+ if (playerRole == "TANK") then
+ playerRole = "DAMAGER"
+ end
+
+ local raidName = GetInstanceInfo()
+ local func = {Details.OpenRaidHistoryWindow, Details, raidName, encounterId, diffName, playerRole, guildName}
+ local icon = {[[Interface\PvPRankBadges\PvPRank08]], 16, 16, false, 0, 1, 0, 1}
+ if (not Details.deny_score_messages) then
+ instanceObject:InstanceAlert(Loc ["STRING_GUILDDAMAGERANK_WINDOWALERT"], icon, Details.update_warning_timeout, func, true)
+ end
+ end
+ end
+ else
+ if (Details.debug) then
+ print("|cFFFFFF00Details! Storage|r: raid difficulty must be heroic or mythic.")
+ end
+ end
+end
diff --git a/luaserver.lua b/luaserver.lua
index abcb5d954..2860fd9e3 100644
--- a/luaserver.lua
+++ b/luaserver.lua
@@ -269,6 +269,13 @@ function LibStub:IterateLibraries()end
---| "INCLUDE_NAME_PLATE_ONLY"
---| "MAW"
+---@class backdrop : table
+---@field bgFile string?
+---@field edgeFile string?
+---@field tile boolean?
+---@field edgeSize number?
+---@field insets table?
+
---@class spellinfo : table
---@field name string
---@field iconID number
@@ -566,6 +573,7 @@ BackdropTemplateMixin = {}
---@field SetToplevel fun(self: frame, toplevel: boolean)
---@field SetPropagateKeyboardInput fun(self: frame, propagate: boolean)
---@field SetPropagateGamepadInput fun(self: frame, propagate: boolean)
+---@field SetMouseClickEnabled fun(self: frame, enabled: boolean)
---@field StartMoving fun(self: frame)
---@field IsMovable fun(self: frame) : boolean
---@field StartSizing fun(self: frame, sizingpoint: sizingpoint?)
@@ -729,7 +737,7 @@ BackdropTemplateMixin = {}
---@field AddMaskTexture fun(self: texture, maskTexture: texture)
---@field SetDrawLayer fun(self: texture, layer: drawlayer, subLayer: number?)
---@field GetTexture fun(self: texture) : any
----@field SetTexture fun(self: texture, path: textureid|texturepath, horizontalWrap: texturewrap?, verticalWrap: texturewrap?, filter: texturefilter?)
+---@field SetTexture fun(self: texture, path: textureid|texturepath?, horizontalWrap: texturewrap?, verticalWrap: texturewrap?, filter: texturefilter?)
---@field SetAtlas fun(self: texture, atlas: string, useAtlasSize: boolean?, filterMode: texturefilter?, resetTexCoords: boolean?)
---@field SetColorTexture fun(self: texture, r: red|number, g: green|number, b: blue|number, a: alpha|number?)
---@field SetDesaturated fun(self: texture, desaturate: boolean)
@@ -5302,7 +5310,7 @@ GetSpellLink = function(spellID) return "" end
---@return string, string, number, number
GetSpellTabInfo = function(tabIndex, isFlyout) return "", "", 0, 0 end
----@param spellName string
+---@param spellName string|number
---@return string
GetSpellTexture = function(spellName) return "" end