diff --git a/Broker.lua b/Broker.lua index 5e648c9..d7843cc 100644 --- a/Broker.lua +++ b/Broker.lua @@ -1,174 +1,174 @@ ---[[ - * Copyright (c) 2011-2020 by Adam Hellberg. - * - * This file is part of KillTrack. - * - * KillTrack is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KillTrack is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KillTrack. If not, see . ---]] - ----@class KillTrack -local KT = select(2, ...) - ----@class KillTrackBroker -local KTB = { - Text = { - Short = "KPM: %.2f", - Long = "Kills Per Minute: %.2f" - } -} - -KT.Broker = KTB -local KTT = KT.Tools - -local UPDATE = 1 -local t = 0 - -local ldb = LibStub:GetLibrary("LibDataBroker-1.1") -local icon = LibStub:GetLibrary("LibDBIcon-1.0") - -local frame = CreateFrame("Frame") - -local tooltipVisible = false - -local data = { - type = "data source", - label = KT.Name, - icon = "Interface\\AddOns\\KillTrack\\icon.tga", - tocname = KT.Name -} - -local clickFunctions = { - none = { - LeftButton = function() KT.MobList:Toggle() end, - MiddleButton = function() KTB:ToggleTextMode() end, - RightButton = function() KT:ResetSession() end - }, - ctrl = { - LeftButton = function() KT:Announce("GROUP") end, -- Announce to group/say - MiddleButton = function() KT.Immediate:Toggle() end, -- Show the immediate frame - RightButton = function() KT:Announce("GUILD") end -- Announce to guild - }, - shift = { - LeftButton = function() KT.Options:Open() end - } -} - -local obj = ldb:NewDataObject("Broker_KillTrack", data) --[[@as LibDataBroker.DataDisplay]] - ----@param self GameTooltip -function obj.OnTooltipShow(self) - self:AddLine(("%s |cff00FF00(%s)|r"):format(KT.Name, KT.Version), 1, 1, 1) - self:AddLine(" ") - local _, kpm, kph, length = KT:GetSessionStats() - self:AddDoubleLine("Session Length", ("|cffFFFFFF%s|r"):format(KTT:FormatSeconds(length))) - self:AddDoubleLine("Kills Per Minute", ("|cffFFFFFF%.2f|r"):format(kpm)) - self:AddDoubleLine("Kills Per Hour", ("|cffFFFFFF%.2f|r"):format(kph)) - self:AddLine(" ") - self:AddDoubleLine("Kills this session", ("|cffFFFFFF%d|r"):format(KT.Session.Count), 1, 1, 0) - local added = 0 - for _, v in pairs(KT:GetSortedSessionKills()) do - self:AddDoubleLine(v.Name, ("|cffFFFFFF%d|r"):format(v.Kills)) - added = added + 1 - end - if added <= 0 then - self:AddLine("No kills this session", 1, 0, 0) - end - self:AddLine(" ") - self:AddDoubleLine("Kills in total", ("|cffFFFFFF%d|r"):format(KT:GetTotalKills()), 1, 1, 0) - added = 0 - for _, v in pairs(KT:GetSortedMobTable()) do - self:AddDoubleLine(v.Name, ("|cffFFFFFF%d (%d)|r"):format(v.cKills, v.gKills)) - added = added + 1 - if added >= 3 then break end - end - if added <= 0 then - self:AddLine("No kills recorded yet", 1, 0, 0) - end - self:AddLine(" ") - self:AddDoubleLine("Left Click", "Open mob database", 0, 1, 0, 0, 1, 0) - self:AddDoubleLine("Middle Click", "Toggle short/long text", 0, 1, 0, 0, 1, 0) - self:AddDoubleLine("Right Click", "Reset session statistics", 0, 1, 0, 0, 1, 0) - self:AddDoubleLine("Ctrl + Left Click", "Announce to group/say", 0, 1, 0, 0, 1, 0) - self:AddDoubleLine("Ctrl + Middle Click", "Toggle immediate frame", 0, 1, 0, 0, 1, 0) - self:AddDoubleLine("Ctrl + Right Click", "Announce to guild", 0, 1, 0, 0, 1, 0) - self:AddDoubleLine("Shift + Left Click", "Open options panel", 0, 1, 0, 0, 1, 0) -end - -function obj:OnClick(button) - local mod = ((IsControlKeyDown() and "ctrl") or (IsShiftKeyDown() and "shift")) or "none" - if not clickFunctions[mod] then return end - local func = clickFunctions[mod][button] - if func then func() end -end - -function obj:OnEnter() - KT:DebugMsg("Broker OnEnter") - GameTooltip:SetOwner(self --[[@as Frame]], "ANCHOR_NONE") - GameTooltip:SetPoint("TOPLEFT", self, "BOTTOMLEFT") - KTB:UpdateTooltip() - tooltipVisible = true -end - -function obj:OnLeave() - KT:DebugMsg("Broker OnLeave") - tooltipVisible = false - GameTooltip:Hide() -end - -function KTB:UpdateText() - local text = KT.Global.BROKER.SHORT_TEXT and self.Text.Short or self.Text.Long - obj.text = text:format(select(2, KT:GetSessionStats())) -end - -function KTB:UpdateTooltip() - KT:DebugMsg("Broker UpdateTooltip") - GameTooltip:ClearLines() - obj.OnTooltipShow(GameTooltip) - GameTooltip:Show() -end - ----@param _ Frame ----@param elapsed number -function KTB:OnUpdate(_, elapsed) - t = t + elapsed - if t >= UPDATE then - self:UpdateText() - if tooltipVisible then self:UpdateTooltip() end - t = 0 - end -end - -function KTB:ToggleTextMode() - KT.Global.BROKER.SHORT_TEXT = not KT.Global.BROKER.SHORT_TEXT - self:UpdateText() -end - -function KTB:OnLoad() - if type(KT.Global.BROKER.MINIMAP.hide) ~= "boolean" then - KT.Global.BROKER.MINIMAP.hide = true - end - icon:Register(KT.Name, obj, KT.Global.BROKER.MINIMAP) - frame:SetScript("OnUpdate", function(f, elapsed) KTB:OnUpdate(f, elapsed) end) - self:UpdateText() -end - ----@param enabled boolean -function KTB:SetMinimap(enabled) - KT.Global.BROKER.MINIMAP.hide = not enabled - if enabled then - icon:Show(KT.Name) - else - icon:Hide(KT.Name) - end -end +--[[ + * Copyright (c) 2011-2020 by Adam Hellberg. + * + * This file is part of KillTrack. + * + * KillTrack is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KillTrack is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KillTrack. If not, see . +--]] + +---@class KillTrack +local KT = select(2, ...) + +---@class KillTrackBroker +local KTB = { + Text = { + Short = "KPM: %.2f", + Long = "Kills Per Minute: %.2f" + } +} + +KT.Broker = KTB +local KTT = KT.Tools + +local UPDATE = 1 +local t = 0 + +local ldb = LibStub:GetLibrary("LibDataBroker-1.1") +local icon = LibStub:GetLibrary("LibDBIcon-1.0") + +local frame = CreateFrame("Frame") + +local tooltipVisible = false + +local data = { + type = "data source", + label = KT.Name, + icon = "Interface\\AddOns\\KillTrack\\icon.tga", + tocname = KT.Name +} + +local clickFunctions = { + none = { + LeftButton = function() KT.MobList:Toggle() end, + MiddleButton = function() KTB:ToggleTextMode() end, + RightButton = function() KT:ResetSession() end + }, + ctrl = { + LeftButton = function() KT:Announce("GROUP") end, -- Announce to group/say + MiddleButton = function() KT.Immediate:Toggle() end, -- Show the immediate frame + RightButton = function() KT:Announce("GUILD") end -- Announce to guild + }, + shift = { + LeftButton = function() KT.Options:Open() end + } +} + +local obj = ldb:NewDataObject("Broker_KillTrack", data) --[[@as LibDataBroker.DataDisplay]] + +---@param self GameTooltip +function obj.OnTooltipShow(self) + self:AddLine(("%s |cff00FF00(%s)|r"):format(KT.Name, KT.Version), 1, 1, 1) + self:AddLine(" ") + local _, kpm, kph, length = KT:GetSessionStats() + self:AddDoubleLine("Session Length", ("|cffFFFFFF%s|r"):format(KTT:FormatSeconds(length))) + self:AddDoubleLine("Kills Per Minute", ("|cffFFFFFF%.2f|r"):format(kpm)) + self:AddDoubleLine("Kills Per Hour", ("|cffFFFFFF%.2f|r"):format(kph)) + self:AddLine(" ") + self:AddDoubleLine("Kills this session", ("|cffFFFFFF%d|r"):format(KT.Session.Count), 1, 1, 0) + local added = 0 + for _, v in pairs(KT:GetSortedSessionKills()) do + self:AddDoubleLine(v.Name, ("|cffFFFFFF%d|r"):format(v.Kills)) + added = added + 1 + end + if added <= 0 then + self:AddLine("No kills this session", 1, 0, 0) + end + self:AddLine(" ") + self:AddDoubleLine("Kills in total", ("|cffFFFFFF%d|r"):format(KT:GetTotalKills()), 1, 1, 0) + added = 0 + for _, v in pairs(KT:GetSortedMobTable()) do + self:AddDoubleLine(v.Name, ("|cffFFFFFF%d (%d)|r"):format(v.cKills, v.gKills)) + added = added + 1 + if added >= 3 then break end + end + if added <= 0 then + self:AddLine("No kills recorded yet", 1, 0, 0) + end + self:AddLine(" ") + self:AddDoubleLine("Left Click", "Open mob database", 0, 1, 0, 0, 1, 0) + self:AddDoubleLine("Middle Click", "Toggle short/long text", 0, 1, 0, 0, 1, 0) + self:AddDoubleLine("Right Click", "Reset session statistics", 0, 1, 0, 0, 1, 0) + self:AddDoubleLine("Ctrl + Left Click", "Announce to group/say", 0, 1, 0, 0, 1, 0) + self:AddDoubleLine("Ctrl + Middle Click", "Toggle immediate frame", 0, 1, 0, 0, 1, 0) + self:AddDoubleLine("Ctrl + Right Click", "Announce to guild", 0, 1, 0, 0, 1, 0) + self:AddDoubleLine("Shift + Left Click", "Open options panel", 0, 1, 0, 0, 1, 0) +end + +function obj:OnClick(button) + local mod = ((IsControlKeyDown() and "ctrl") or (IsShiftKeyDown() and "shift")) or "none" + if not clickFunctions[mod] then return end + local func = clickFunctions[mod][button] + if func then func() end +end + +function obj:OnEnter() + KT:DebugMsg("Broker OnEnter") + GameTooltip:SetOwner(self --[[@as Frame]], "ANCHOR_NONE") + GameTooltip:SetPoint("TOPLEFT", self, "BOTTOMLEFT") + KTB:UpdateTooltip() + tooltipVisible = true +end + +function obj:OnLeave() + KT:DebugMsg("Broker OnLeave") + tooltipVisible = false + GameTooltip:Hide() +end + +function KTB:UpdateText() + local text = KT.Global.BROKER.SHORT_TEXT and self.Text.Short or self.Text.Long + obj.text = text:format(select(2, KT:GetSessionStats())) +end + +function KTB:UpdateTooltip() + KT:DebugMsg("Broker UpdateTooltip") + GameTooltip:ClearLines() + obj.OnTooltipShow(GameTooltip) + GameTooltip:Show() +end + +---@param _ Frame +---@param elapsed number +function KTB:OnUpdate(_, elapsed) + t = t + elapsed + if t >= UPDATE then + self:UpdateText() + if tooltipVisible then self:UpdateTooltip() end + t = 0 + end +end + +function KTB:ToggleTextMode() + KT.Global.BROKER.SHORT_TEXT = not KT.Global.BROKER.SHORT_TEXT + self:UpdateText() +end + +function KTB:OnLoad() + if type(KT.Global.BROKER.MINIMAP.hide) ~= "boolean" then + KT.Global.BROKER.MINIMAP.hide = true + end + icon:Register(KT.Name, obj, KT.Global.BROKER.MINIMAP) + frame:SetScript("OnUpdate", function(f, elapsed) KTB:OnUpdate(f, elapsed) end) + self:UpdateText() +end + +---@param enabled boolean +function KTB:SetMinimap(enabled) + KT.Global.BROKER.MINIMAP.hide = not enabled + if enabled then + icon:Show(KT.Name) + else + icon:Hide(KT.Name) + end +end diff --git a/Command.lua b/Command.lua index 640d765..151dc44 100644 --- a/Command.lua +++ b/Command.lua @@ -1,296 +1,296 @@ ---[[ - * Copyright (c) 2011-2020 by Adam Hellberg. - * - * This file is part of KillTrack. - * - * KillTrack is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KillTrack is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KillTrack. If not, see . ---]] - ----@class KillTrack -local KT = select(2, ...) - ----@class KillTrackCommand -local C = { - Slash = { - "killtrack", - "kt" - }, - ---@type { [string]: fun(args: string[]) } - Commands = {} -} - -KT.Command = C -local KTT = KT.Tools - --- Argument #1 (command) can be either string or a table. ----@param command string|string[] ----@param func fun(args: string[]) -function C:Register(command, func) - if type(command) == "string" then - command = {command} - end - for _,v in pairs(command) do - if not self:HasCommand(v) then - if v ~= "__DEFAULT__" then v = v:lower() end - self.Commands[v] = func - end - end -end - ----@param command string ----@return boolean -function C:HasCommand(command) - for k,_ in pairs(self.Commands) do - if k == command then return true end - end - return false -end - ----@param command string ----@return fun(args: string[]) -function C:GetCommand(command) - local cmd = self.Commands[command] - if cmd then return cmd else return self.Commands["__DEFAULT__"] end -end - ----@param command string ----@param args string[] -function C:HandleCommand(command, args) - local cmd = self:GetCommand(command) - if cmd then - cmd(args) - else - KT:Msg(("%q is not a valid command."):format(command)) - end -end - -C:Register("__DEFAULT__", function() - KT:Msg("/kt loadmessage - Toggles showing a message when AddOn loads.") - KT:Msg("/kt target - Display number of kills on target mob.") - KT:Msg("/kt lookup - Display number of kills on , can also be NPC ID.") - KT:Msg("/kt print - Toggle printing kill updates to chat.") - KT:Msg("/kt list - Display a list of all mobs entries.") - KT:Msg("/kt set - Set kill counts for a mob") - KT:Msg("/kt delete - Delete entry with NPC id .") - KT:Msg("/kt purge [threshold] - Open dialog to purge entries, specifiying a threshold here is optional.") - KT:Msg("/kt reset - Clear the mob database.") - KT:Msg("/kt time - Track kills within specified time.") - KT:Msg("/kt immediate - Track kills made from this point on.") - KT:Msg("/kt threshold - Set threshold for kill record notices to show.") - KT:Msg("/kt countmode - Toggle between counting group killing blows or your killing blows only.") - KT:Msg("/kt minimap - Toggles the minimap icon") - KT:Msg("/kt tooltip - Toggles showing mob data in tooltip") - KT:Msg("/kt - Displays this help message.") -end) - -C:Register({"loadmessage", "lm"}, function() - KT:ToggleLoadMessage() -end) - -C:Register({"target", "t", "tar"}, function() - if not UnitExists("target") or UnitIsPlayer("target") then return end - local id = KTT.GUIDToID(UnitGUID("target")) - KT:PrintKills(id) -end) - -C:Register({"print", "p"}, function() - KT.Global.PRINTKILLS = not KT.Global.PRINTKILLS - if KT.Global.PRINTKILLS then - KT:Msg("Announcing kill updates.") - else - KT:Msg("No longer announcing kill updates.") - end -end) - -C:Register({"printnew", "pn"}, function() - KT.Global.PRINTNEW = not KT.Global.PRINTNEW - if KT.Global.PRINTNEW then - KT:Msg("Announcing new mob entries") - else - KT:Msg("No longer announcing new mob entries") - end -end) - -C:Register({"set", "edit"}, function(args) - local id = tonumber(args[1]) --[[@as integer]] - local name = args[2] - local global = tonumber(args[3]) --[[@as integer]] - local char = tonumber(args[4]) --[[@as integer]] - - local err - - if not id then - KT:Msg("Missing or invalid argument: id") - err = true - end - - if not name then - KT:Msg("Missing or invalid argument: name") - err = true - end - - if not global then - KT:Msg("Missing or invalid argument: global") - err = true - end - - if not char then - KT:Msg("Missing or invalid argument: char") - err = true - end - - if err then - KT:Msg("Usage: /kt set ") - return - end - - KT:SetKills(id, name, global, char) -end) - -C:Register({"delete", "del", "remove", "rem"}, function(args) - if #args <= 0 then - KT:Msg("Missing argument: id") - return - end - local id = tonumber(args[1]) - if not id then - KT:Msg("Id must be a number") - return - end - if not KT.Global.MOBS[id] then - KT:Msg(("Id %d does not exist in the database."):format(id)) - return - end - local name = KT.Global.MOBS[id].Name - KT:ShowDelete(id, name) -end) - -C:Register({"purge"}, function(args) - ---@type integer? - local threshold - if #args >= 1 then threshold = tonumber(args[1]) --[[@as integer]] end - KT:ShowPurge(threshold) -end) - -C:Register({"reset", "r"}, function() KT:ShowReset() end) - -C:Register({"lookup", "lo", "check"}, function(args) - if #args <= 0 then - KT:Msg("Missing argument: name") - return - end - local name = table.concat(args, " ") - KT:PrintKills(name) -end) - -C:Register({"list", "moblist", "mobs"}, function() KT.MobList:Show() end) - -C:Register({"time", "timer"}, function(args) - if #args <= 0 then - KT:Msg("Usage: time [minutes] [hours]") - KT:Msg("Usage: time [s][m][h]") - return - end - - local s, m, h - - if #args == 1 then - if not tonumber(args[1]) then - args[1] = args[1]:lower() - s = args[1]:match("(%d+)s") - m = args[1]:match("(%d+)m") - h = args[1]:match("(%d+)h") - if not s and not m and not h then - KT:Msg("Invalid number format.") - return - end - else - s = tonumber(args[1]) - end - else - s = tonumber(args[1]) - m = tonumber(args[2]) - h = tonumber(args[3]) - end - KT.TimerFrame:Start(s, m, h) -end) - -C:Register({"immediate", "imm", "i"}, function(args) - if #args < 1 then - KT.Immediate:Show() - elseif args[1]:match("^t") then - local threshold = tonumber(args[2]) --[[@as integer]] - if #args < 2 then - KT:Msg("Usage: immediate threshold ") - KT:Msg("E.g: /kt immediate threshold 50") - KT:Msg(" Notice will be shown on every 50th kill when immediate frame is active") - else - KT:SetImmediateThreshold(threshold) - end - elseif args[1]:match("^f") then - if #args < 2 then - local current = KT.Global.IMMEDIATE.FILTER - KT:Msg("The current immediate filter is set to: " .. (current and current or "")) - KT:Msg("Use /kt immediate filter to set a new filter") - KT:Msg("Use /kt immediate clearfilter to clear the filter") - else - KT:SetImmediateFilter(tostring(args[2])) - end - elseif args[1]:match("^c") then - KT:ClearImmediateFilter() - end -end) - -C:Register({"threshold"}, function(args) - if #args <= 0 then - KT:Msg("Usage: threshold ") - KT:Msg("E.g: /kt threshold 100") - KT:Msg(" Notice will be shown on every 100th kill.") - return - end - local t = tonumber(args[1]) - if t then - KT:SetThreshold(t) - else - KT:Msg("Argument must be a number.") - end -end) - -C:Register({"countmode", "cm", "count", "counttype", "changecount", "switchcount"}, function() KT:ToggleCountMode() end) -C:Register({"debug", "toggledebug", "d", "td"}, function() KT:ToggleDebug() end) -C:Register({"exp", "xp", "experience", "shoexp", "showxp"}, function() KT:ToggleExp() end) -C:Register({"options", "opt", "config", "conf"}, function() KT.Options:Open() end) -C:Register({"minimap", "mp"}, function() KT.Broker:SetMinimap(KT.Global.BROKER.MINIMAP.hide) end) - -C:Register({"tooltip", "tt"}, function() - KT.Global.TOOLTIP = not KT.Global.TOOLTIP - KT:Msg("Tooltip data " .. (KT.Global.TOOLTIP and "enabled" or "disabled")) -end) - -for i,v in ipairs(C.Slash) do - _G["SLASH_" .. KT.Name:upper() .. i] = "/" .. v -end - -SlashCmdList[KT.Name:upper()] = function(msg) - msg = KTT:Trim(msg) - local args = KTT:Split(msg) - local cmd = args[1] - local t = {} - if #args > 1 then - for i = 2, #args do - t[#t + 1] = args[i] - end - end - C:HandleCommand(cmd, t) -end +--[[ + * Copyright (c) 2011-2020 by Adam Hellberg. + * + * This file is part of KillTrack. + * + * KillTrack is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KillTrack is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KillTrack. If not, see . +--]] + +---@class KillTrack +local KT = select(2, ...) + +---@class KillTrackCommand +local C = { + Slash = { + "killtrack", + "kt" + }, + ---@type { [string]: fun(args: string[]) } + Commands = {} +} + +KT.Command = C +local KTT = KT.Tools + +-- Argument #1 (command) can be either string or a table. +---@param command string|string[] +---@param func fun(args: string[]) +function C:Register(command, func) + if type(command) == "string" then + command = {command} + end + for _,v in pairs(command) do + if not self:HasCommand(v) then + if v ~= "__DEFAULT__" then v = v:lower() end + self.Commands[v] = func + end + end +end + +---@param command string +---@return boolean +function C:HasCommand(command) + for k,_ in pairs(self.Commands) do + if k == command then return true end + end + return false +end + +---@param command string +---@return fun(args: string[]) +function C:GetCommand(command) + local cmd = self.Commands[command] + if cmd then return cmd else return self.Commands["__DEFAULT__"] end +end + +---@param command string +---@param args string[] +function C:HandleCommand(command, args) + local cmd = self:GetCommand(command) + if cmd then + cmd(args) + else + KT:Msg(("%q is not a valid command."):format(command)) + end +end + +C:Register("__DEFAULT__", function() + KT:Msg("/kt loadmessage - Toggles showing a message when AddOn loads.") + KT:Msg("/kt target - Display number of kills on target mob.") + KT:Msg("/kt lookup - Display number of kills on , can also be NPC ID.") + KT:Msg("/kt print - Toggle printing kill updates to chat.") + KT:Msg("/kt list - Display a list of all mobs entries.") + KT:Msg("/kt set - Set kill counts for a mob") + KT:Msg("/kt delete - Delete entry with NPC id .") + KT:Msg("/kt purge [threshold] - Open dialog to purge entries, specifiying a threshold here is optional.") + KT:Msg("/kt reset - Clear the mob database.") + KT:Msg("/kt time - Track kills within specified time.") + KT:Msg("/kt immediate - Track kills made from this point on.") + KT:Msg("/kt threshold - Set threshold for kill record notices to show.") + KT:Msg("/kt countmode - Toggle between counting group killing blows or your killing blows only.") + KT:Msg("/kt minimap - Toggles the minimap icon") + KT:Msg("/kt tooltip - Toggles showing mob data in tooltip") + KT:Msg("/kt - Displays this help message.") +end) + +C:Register({"loadmessage", "lm"}, function() + KT:ToggleLoadMessage() +end) + +C:Register({"target", "t", "tar"}, function() + if not UnitExists("target") or UnitIsPlayer("target") then return end + local id = KTT.GUIDToID(UnitGUID("target")) + KT:PrintKills(id) +end) + +C:Register({"print", "p"}, function() + KT.Global.PRINTKILLS = not KT.Global.PRINTKILLS + if KT.Global.PRINTKILLS then + KT:Msg("Announcing kill updates.") + else + KT:Msg("No longer announcing kill updates.") + end +end) + +C:Register({"printnew", "pn"}, function() + KT.Global.PRINTNEW = not KT.Global.PRINTNEW + if KT.Global.PRINTNEW then + KT:Msg("Announcing new mob entries") + else + KT:Msg("No longer announcing new mob entries") + end +end) + +C:Register({"set", "edit"}, function(args) + local id = tonumber(args[1]) --[[@as integer]] + local name = args[2] + local global = tonumber(args[3]) --[[@as integer]] + local char = tonumber(args[4]) --[[@as integer]] + + local err + + if not id then + KT:Msg("Missing or invalid argument: id") + err = true + end + + if not name then + KT:Msg("Missing or invalid argument: name") + err = true + end + + if not global then + KT:Msg("Missing or invalid argument: global") + err = true + end + + if not char then + KT:Msg("Missing or invalid argument: char") + err = true + end + + if err then + KT:Msg("Usage: /kt set ") + return + end + + KT:SetKills(id, name, global, char) +end) + +C:Register({"delete", "del", "remove", "rem"}, function(args) + if #args <= 0 then + KT:Msg("Missing argument: id") + return + end + local id = tonumber(args[1]) + if not id then + KT:Msg("Id must be a number") + return + end + if not KT.Global.MOBS[id] then + KT:Msg(("Id %d does not exist in the database."):format(id)) + return + end + local name = KT.Global.MOBS[id].Name + KT:ShowDelete(id, name) +end) + +C:Register({"purge"}, function(args) + ---@type integer? + local threshold + if #args >= 1 then threshold = tonumber(args[1]) --[[@as integer]] end + KT:ShowPurge(threshold) +end) + +C:Register({"reset", "r"}, function() KT:ShowReset() end) + +C:Register({"lookup", "lo", "check"}, function(args) + if #args <= 0 then + KT:Msg("Missing argument: name") + return + end + local name = table.concat(args, " ") + KT:PrintKills(name) +end) + +C:Register({"list", "moblist", "mobs"}, function() KT.MobList:Show() end) + +C:Register({"time", "timer"}, function(args) + if #args <= 0 then + KT:Msg("Usage: time [minutes] [hours]") + KT:Msg("Usage: time [s][m][h]") + return + end + + local s, m, h + + if #args == 1 then + if not tonumber(args[1]) then + args[1] = args[1]:lower() + s = args[1]:match("(%d+)s") + m = args[1]:match("(%d+)m") + h = args[1]:match("(%d+)h") + if not s and not m and not h then + KT:Msg("Invalid number format.") + return + end + else + s = tonumber(args[1]) + end + else + s = tonumber(args[1]) + m = tonumber(args[2]) + h = tonumber(args[3]) + end + KT.TimerFrame:Start(s, m, h) +end) + +C:Register({"immediate", "imm", "i"}, function(args) + if #args < 1 then + KT.Immediate:Show() + elseif args[1]:match("^t") then + local threshold = tonumber(args[2]) --[[@as integer]] + if #args < 2 then + KT:Msg("Usage: immediate threshold ") + KT:Msg("E.g: /kt immediate threshold 50") + KT:Msg(" Notice will be shown on every 50th kill when immediate frame is active") + else + KT:SetImmediateThreshold(threshold) + end + elseif args[1]:match("^f") then + if #args < 2 then + local current = KT.Global.IMMEDIATE.FILTER + KT:Msg("The current immediate filter is set to: " .. (current and current or "")) + KT:Msg("Use /kt immediate filter to set a new filter") + KT:Msg("Use /kt immediate clearfilter to clear the filter") + else + KT:SetImmediateFilter(tostring(args[2])) + end + elseif args[1]:match("^c") then + KT:ClearImmediateFilter() + end +end) + +C:Register({"threshold"}, function(args) + if #args <= 0 then + KT:Msg("Usage: threshold ") + KT:Msg("E.g: /kt threshold 100") + KT:Msg(" Notice will be shown on every 100th kill.") + return + end + local t = tonumber(args[1]) + if t then + KT:SetThreshold(t) + else + KT:Msg("Argument must be a number.") + end +end) + +C:Register({"countmode", "cm", "count", "counttype", "changecount", "switchcount"}, function() KT:ToggleCountMode() end) +C:Register({"debug", "toggledebug", "d", "td"}, function() KT:ToggleDebug() end) +C:Register({"exp", "xp", "experience", "shoexp", "showxp"}, function() KT:ToggleExp() end) +C:Register({"options", "opt", "config", "conf"}, function() KT.Options:Open() end) +C:Register({"minimap", "mp"}, function() KT.Broker:SetMinimap(KT.Global.BROKER.MINIMAP.hide) end) + +C:Register({"tooltip", "tt"}, function() + KT.Global.TOOLTIP = not KT.Global.TOOLTIP + KT:Msg("Tooltip data " .. (KT.Global.TOOLTIP and "enabled" or "disabled")) +end) + +for i,v in ipairs(C.Slash) do + _G["SLASH_" .. KT.Name:upper() .. i] = "/" .. v +end + +SlashCmdList[KT.Name:upper()] = function(msg) + msg = KTT:Trim(msg) + local args = KTT:Split(msg) + local cmd = args[1] + local t = {} + if #args > 1 then + for i = 2, #args do + t[#t + 1] = args[i] + end + end + C:HandleCommand(cmd, t) +end diff --git a/Dialogs.lua b/Dialogs.lua index 427e388..fd107a0 100644 --- a/Dialogs.lua +++ b/Dialogs.lua @@ -1,107 +1,107 @@ ---[[ - * Copyright (c) 2011-2020 by Adam Hellberg. - * - * This file is part of KillTrack. - * - * KillTrack is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KillTrack is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KillTrack. If not, see . ---]] - ----@class KillTrack -local KT = select(2, ...) - -StaticPopupDialogs.KILLTRACK_FINISH = { - text = "%s entries removed.", - button1 = "Okay", - timeout = 10, - enterClicksFirstButton = true, - whileDead = true, - hideOnEscape = true -} - -StaticPopupDialogs.KILLTRACK_DELETE = { - text = "Delete %s with ID %s?", - button1 = "Delete all", - button2 = "CANCEL", - button3 = "Character Only", - OnAccept = function() KT:Delete(KT.Temp.DeleteId) KT.MobList:UpdateMobs() KT.MobList:UpdateEntries() end, - OnAlt = function() KT:Delete(KT.Temp.DeleteId, true) KT.MobList:UpdateMobs() KT.MobList:UpdateEntries() end, - showAlert = true, - timeout = 10, - whileDead = true, - hideOnEscape = true -} - -StaticPopupDialogs.KILLTRACK_PURGE = { - text = "Remove all mob entries with their kill count below this threshold:", - button1 = "Purge", - button2 = "Cancel", - hasEditBox = true, - OnAccept = function(self) - KT:Purge(tonumber(self.editBox:GetText()) --[[@as integer]]) KT.MobList:UpdateMobs() KT.MobList:UpdateEntries() - end, - OnCancel = function() KT.Temp.Threshold = nil end, - OnShow = function(self) - if tonumber(KT.Temp.Threshold) then - self.editBox:SetText(tostring(KT.Temp.Threshold)) - else - self.button1:Disable() - end - end, - EditBoxOnTextChanged = function(self) - if tonumber(self:GetText()) then - self:GetParent().button1:Enable() - else - self:GetParent().button1:Disable() - end - end, - showAlert = true, - enterClicksFirstButton = true, - timeout = 0, - whileDead = true, - hideOnEscape = true -} - -StaticPopupDialogs.KILLTRACK_RESET = { - text = "Remove all mob entries from the database? THIS CANNOT BE REVERSED.", - button1 = "Yes", - button2 = "No", - OnAccept = function() KT:Reset() KT.MobList:UpdateMobs() KT.MobList:UpdateEntries() end, - showAlert = true, - enterClicksFirstButton = true, - timeout = 0, - whileDead = true, - hideOnEscape = true -} - ----@param id integer|string ----@param name string -function KT:ShowDelete(id, name) - id = tonumber(id) --[[@as integer]] - name = tostring(name) - if not id then error("'id' must be a number.") end - self.Temp.DeleteId = id - StaticPopup_Show("KILLTRACK_DELETE", name, id) -end - ----@param threshold integer? -function KT:ShowPurge(threshold) - if tonumber(threshold) then - self.Temp.Threshold = tonumber(threshold) - end - StaticPopup_Show("KILLTRACK_PURGE") -end - -function KT:ShowReset() - StaticPopup_Show("KILLTRACK_RESET") -end +--[[ + * Copyright (c) 2011-2020 by Adam Hellberg. + * + * This file is part of KillTrack. + * + * KillTrack is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KillTrack is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KillTrack. If not, see . +--]] + +---@class KillTrack +local KT = select(2, ...) + +StaticPopupDialogs.KILLTRACK_FINISH = { + text = "%s entries removed.", + button1 = "Okay", + timeout = 10, + enterClicksFirstButton = true, + whileDead = true, + hideOnEscape = true +} + +StaticPopupDialogs.KILLTRACK_DELETE = { + text = "Delete %s with ID %s?", + button1 = "Delete all", + button2 = "CANCEL", + button3 = "Character Only", + OnAccept = function() KT:Delete(KT.Temp.DeleteId) KT.MobList:UpdateMobs() KT.MobList:UpdateEntries() end, + OnAlt = function() KT:Delete(KT.Temp.DeleteId, true) KT.MobList:UpdateMobs() KT.MobList:UpdateEntries() end, + showAlert = true, + timeout = 10, + whileDead = true, + hideOnEscape = true +} + +StaticPopupDialogs.KILLTRACK_PURGE = { + text = "Remove all mob entries with their kill count below this threshold:", + button1 = "Purge", + button2 = "Cancel", + hasEditBox = true, + OnAccept = function(self) + KT:Purge(tonumber(self.editBox:GetText()) --[[@as integer]]) KT.MobList:UpdateMobs() KT.MobList:UpdateEntries() + end, + OnCancel = function() KT.Temp.Threshold = nil end, + OnShow = function(self) + if tonumber(KT.Temp.Threshold) then + self.editBox:SetText(tostring(KT.Temp.Threshold)) + else + self.button1:Disable() + end + end, + EditBoxOnTextChanged = function(self) + if tonumber(self:GetText()) then + self:GetParent().button1:Enable() + else + self:GetParent().button1:Disable() + end + end, + showAlert = true, + enterClicksFirstButton = true, + timeout = 0, + whileDead = true, + hideOnEscape = true +} + +StaticPopupDialogs.KILLTRACK_RESET = { + text = "Remove all mob entries from the database? THIS CANNOT BE REVERSED.", + button1 = "Yes", + button2 = "No", + OnAccept = function() KT:Reset() KT.MobList:UpdateMobs() KT.MobList:UpdateEntries() end, + showAlert = true, + enterClicksFirstButton = true, + timeout = 0, + whileDead = true, + hideOnEscape = true +} + +---@param id integer|string +---@param name string +function KT:ShowDelete(id, name) + id = tonumber(id) --[[@as integer]] + name = tostring(name) + if not id then error("'id' must be a number.") end + self.Temp.DeleteId = id + StaticPopup_Show("KILLTRACK_DELETE", name, id) +end + +---@param threshold integer? +function KT:ShowPurge(threshold) + if tonumber(threshold) then + self.Temp.Threshold = tonumber(threshold) + end + StaticPopup_Show("KILLTRACK_PURGE") +end + +function KT:ShowReset() + StaticPopup_Show("KILLTRACK_RESET") +end diff --git a/ExpTracker.lua b/ExpTracker.lua index 12925c7..a2bbd34 100644 --- a/ExpTracker.lua +++ b/ExpTracker.lua @@ -1,73 +1,73 @@ ---[[ - * Copyright (c) 2011-2020 by Adam Hellberg. - * - * This file is part of KillTrack. - * - * KillTrack is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KillTrack is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KillTrack. If not, see . ---]] - ----@class KillTrack -local KT = select(2, ...) - ----@class KillTrackExpTracker -local ET = { - ---@type string[] - Strings = { - ---@diagnostic disable: undefined-field - _G.COMBATLOG_XPGAIN_EXHAUSTION1, -- %s dies, you gain %d experience. (%s exp %s bonus) - _G.COMBATLOG_XPGAIN_EXHAUSTION1_GROUP, -- %s dies, you gain %d experience. (%s exp %s bonus, +%d group bonus) - _G.COMBATLOG_XPGAIN_EXHAUSTION1_RAID, -- %s dies, you gain %d experience. (%s exp %s bonus, -%d raid penalty) - _G.COMBATLOG_XPGAIN_EXHAUSTION2, -- %s dies, you gain %d experience. (%s exp %s bonus) - _G.COMBATLOG_XPGAIN_EXHAUSTION2_GROUP, -- %s dies, you gain %d experience. (%s exp %s bonus, +%d group bonus) - _G.COMBATLOG_XPGAIN_EXHAUSTION2_RAID, -- %s dies, you gain %d experience. (%s exp %s bonus, -%d raid penalty) - _G.COMBATLOG_XPGAIN_EXHAUSTION4, -- %s dies, you gain %d experience. (%s exp %s penalty) - _G.COMBATLOG_XPGAIN_EXHAUSTION4_GROUP, -- %s dies, you gain %d experience. (%s exp %s penalty, +%d group bonus) - _G.COMBATLOG_XPGAIN_EXHAUSTION4_RAID, -- %s dies, you gain %d experience. (%s exp %s penalty, -%d raid penalty) - _G.COMBATLOG_XPGAIN_EXHAUSTION5, -- %s dies, you gain %d experience. (%s exp %s penalty) - _G.COMBATLOG_XPGAIN_EXHAUSTION5_GROUP, -- %s dies, you gain %d experience. (%s exp %s penalty, +%d group bonus) - _G.COMBATLOG_XPGAIN_EXHAUSTION5_RAID, -- %s dies, you gain %d experience. (%s exp %s penalty, -%d raid penalty) - _G.COMBATLOG_XPGAIN_FIRSTPERSON, -- %s dies, you gain %d experience. - _G.COMBATLOG_XPGAIN_FIRSTPERSON_GROUP, -- %s dies, you gain %d experience. (+%d group bonus) - _G.COMBATLOG_XPGAIN_FIRSTPERSON_RAID -- %s dies, you gain %d experience. (-%d raid penalty) - ---@diagnostic enable: undefined-field - } -} - -KT.ExpTracker = ET - -local initialized = false - -local function Initialize() - for i = 1, #ET.Strings do - ET.Strings[i] = ET.Strings[i]:gsub("([%(%)])", "%%%1"):gsub("%%%d?$?s", "(.-)"):gsub("%%%d?$?d", "(%%d+)") - end - initialized = true -end - ----@param message string -function ET:CheckMessage(message) - if not initialized then Initialize() end - local name, exp - for i = 1, #self.Strings do - local str = self.Strings[i] - name, exp = message:match(str) - if name and exp then break end - end - - exp = tonumber(exp) - - if type(name) == "string" and name ~= "" and type(exp) == "number" then - KT:SetExp(name, exp) - end -end +--[[ + * Copyright (c) 2011-2020 by Adam Hellberg. + * + * This file is part of KillTrack. + * + * KillTrack is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KillTrack is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KillTrack. If not, see . +--]] + +---@class KillTrack +local KT = select(2, ...) + +---@class KillTrackExpTracker +local ET = { + ---@type string[] + Strings = { + ---@diagnostic disable: undefined-field + _G.COMBATLOG_XPGAIN_EXHAUSTION1, -- %s dies, you gain %d experience. (%s exp %s bonus) + _G.COMBATLOG_XPGAIN_EXHAUSTION1_GROUP, -- %s dies, you gain %d experience. (%s exp %s bonus, +%d group bonus) + _G.COMBATLOG_XPGAIN_EXHAUSTION1_RAID, -- %s dies, you gain %d experience. (%s exp %s bonus, -%d raid penalty) + _G.COMBATLOG_XPGAIN_EXHAUSTION2, -- %s dies, you gain %d experience. (%s exp %s bonus) + _G.COMBATLOG_XPGAIN_EXHAUSTION2_GROUP, -- %s dies, you gain %d experience. (%s exp %s bonus, +%d group bonus) + _G.COMBATLOG_XPGAIN_EXHAUSTION2_RAID, -- %s dies, you gain %d experience. (%s exp %s bonus, -%d raid penalty) + _G.COMBATLOG_XPGAIN_EXHAUSTION4, -- %s dies, you gain %d experience. (%s exp %s penalty) + _G.COMBATLOG_XPGAIN_EXHAUSTION4_GROUP, -- %s dies, you gain %d experience. (%s exp %s penalty, +%d group bonus) + _G.COMBATLOG_XPGAIN_EXHAUSTION4_RAID, -- %s dies, you gain %d experience. (%s exp %s penalty, -%d raid penalty) + _G.COMBATLOG_XPGAIN_EXHAUSTION5, -- %s dies, you gain %d experience. (%s exp %s penalty) + _G.COMBATLOG_XPGAIN_EXHAUSTION5_GROUP, -- %s dies, you gain %d experience. (%s exp %s penalty, +%d group bonus) + _G.COMBATLOG_XPGAIN_EXHAUSTION5_RAID, -- %s dies, you gain %d experience. (%s exp %s penalty, -%d raid penalty) + _G.COMBATLOG_XPGAIN_FIRSTPERSON, -- %s dies, you gain %d experience. + _G.COMBATLOG_XPGAIN_FIRSTPERSON_GROUP, -- %s dies, you gain %d experience. (+%d group bonus) + _G.COMBATLOG_XPGAIN_FIRSTPERSON_RAID -- %s dies, you gain %d experience. (-%d raid penalty) + ---@diagnostic enable: undefined-field + } +} + +KT.ExpTracker = ET + +local initialized = false + +local function Initialize() + for i = 1, #ET.Strings do + ET.Strings[i] = ET.Strings[i]:gsub("([%(%)])", "%%%1"):gsub("%%%d?$?s", "(.-)"):gsub("%%%d?$?d", "(%%d+)") + end + initialized = true +end + +---@param message string +function ET:CheckMessage(message) + if not initialized then Initialize() end + local name, exp + for i = 1, #self.Strings do + local str = self.Strings[i] + name, exp = message:match(str) + if name and exp then break end + end + + exp = tonumber(exp) + + if type(name) == "string" and name ~= "" and type(exp) == "number" then + KT:SetExp(name, exp) + end +end diff --git a/ImmediateFrame.lua b/ImmediateFrame.lua index dde3dd7..1d570e6 100644 --- a/ImmediateFrame.lua +++ b/ImmediateFrame.lua @@ -1,126 +1,126 @@ ---[[ - * Copyright (c) 2013 by Adam Hellberg. - * - * This file is part of KillTrack. - * - * KillTrack is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KillTrack is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KillTrack. If not, see . ---]] - ----@class KillTrack -local KT = select(2, ...) - ----@class KillTrackImmediateFrame -local I = { - Active = false, - Kills = 0 -} - -KT.Immediate = I - -local frame - -local function SetupFrame() - if frame then return end - local G = KT.Global.IMMEDIATE - frame = CreateFrame("Frame", nil, UIParent, BackdropTemplateMixin and "BackdropTemplate") - frame:Hide() - frame:EnableMouse(true) - frame:SetMovable(true) - if G.POSITION.POINT then - frame:SetPoint(G.POSITION.POINT, UIParent, G.POSITION.RELATIVE, G.POSITION.X, G.POSITION.Y) - else - frame:SetPoint("CENTER") - end - frame:SetWidth(240) - frame:SetHeight(30) - - local bd = { - bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", - edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", - tile = true, - edgeSize = 16, - tileSize = 32, - insets = { - left = 2.5, - right = 2.5, - top = 2.5, - bottom = 2.5 - } - } - - frame:SetBackdrop(bd) - - frame:SetScript("OnMouseDown", function(f) f:StartMoving() end) - frame:SetScript("OnMouseUp", function(f) - f:StopMovingOrSizing() - local point, _, relative, x, y = f:GetPoint() - G.POSITION.POINT = point - G.POSITION.RELATIVE = relative - G.POSITION.X = x - G.POSITION.Y = y - end) - - ---@diagnostic disable-next-line: inject-field - frame.killLabel = frame:CreateFontString(nil, "OVERLAY", nil) - frame.killLabel:SetFont("Fonts\\FRIZQT__.TTF", 16, nil) - frame.killLabel:SetWidth(100) - --frame.killLabel:SetHeight(24) - frame.killLabel:SetPoint("LEFT", frame, "LEFT", 2, 0) - frame.killLabel:SetText("Kills so far:") - - ---@diagnostic disable-next-line: inject-field - frame.killCount = frame:CreateFontString(nil, "OVERLAY", nil) - frame.killCount:SetFont("Fonts\\FRIZQT__.TTF", 16, nil) - frame.killCount:SetWidth(100) - --frame.killCount:SetHeight(24) - frame.killCount:SetPoint("RIGHT", frame, "RIGHT", -68, 0) - frame.killCount:SetJustifyH("RIGHT") - frame.killCount:SetText("0") - - ---@diagnostic disable-next-line: inject-field - frame.closeButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") - frame.closeButton:SetWidth(60) - frame.closeButton:SetHeight(24) - frame.closeButton:SetPoint("RIGHT", frame, "RIGHT", -3, 0) - frame.closeButton:SetText("Close") - frame.closeButton:SetScript("OnClick", function() I:Hide() end) -end - -function I:Show() - if not frame then SetupFrame() end - self.Kills = 0 - frame.killCount:SetText(tostring(self.Kills)) - frame:Show() - self.Active = true -end - -function I:Hide() - frame:Hide() - self.Kills = 0 - frame.killCount:SetText(tostring(self.Kills)) - self.Active = false -end - -function I:Toggle() - if frame and frame:IsShown() then - self:Hide() - else - self:Show() - end -end - -function I:AddKill() - self.Kills = self.Kills + 1 - frame.killCount:SetText(tostring(self.Kills)) -end +--[[ + * Copyright (c) 2013 by Adam Hellberg. + * + * This file is part of KillTrack. + * + * KillTrack is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KillTrack is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KillTrack. If not, see . +--]] + +---@class KillTrack +local KT = select(2, ...) + +---@class KillTrackImmediateFrame +local I = { + Active = false, + Kills = 0 +} + +KT.Immediate = I + +local frame + +local function SetupFrame() + if frame then return end + local G = KT.Global.IMMEDIATE + frame = CreateFrame("Frame", nil, UIParent, BackdropTemplateMixin and "BackdropTemplate") + frame:Hide() + frame:EnableMouse(true) + frame:SetMovable(true) + if G.POSITION.POINT then + frame:SetPoint(G.POSITION.POINT, UIParent, G.POSITION.RELATIVE, G.POSITION.X, G.POSITION.Y) + else + frame:SetPoint("CENTER") + end + frame:SetWidth(240) + frame:SetHeight(30) + + local bd = { + bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, + edgeSize = 16, + tileSize = 32, + insets = { + left = 2.5, + right = 2.5, + top = 2.5, + bottom = 2.5 + } + } + + frame:SetBackdrop(bd) + + frame:SetScript("OnMouseDown", function(f) f:StartMoving() end) + frame:SetScript("OnMouseUp", function(f) + f:StopMovingOrSizing() + local point, _, relative, x, y = f:GetPoint() + G.POSITION.POINT = point + G.POSITION.RELATIVE = relative + G.POSITION.X = x + G.POSITION.Y = y + end) + + ---@diagnostic disable-next-line: inject-field + frame.killLabel = frame:CreateFontString(nil, "OVERLAY", nil) + frame.killLabel:SetFont("Fonts\\FRIZQT__.TTF", 16, nil) + frame.killLabel:SetWidth(100) + --frame.killLabel:SetHeight(24) + frame.killLabel:SetPoint("LEFT", frame, "LEFT", 2, 0) + frame.killLabel:SetText("Kills so far:") + + ---@diagnostic disable-next-line: inject-field + frame.killCount = frame:CreateFontString(nil, "OVERLAY", nil) + frame.killCount:SetFont("Fonts\\FRIZQT__.TTF", 16, nil) + frame.killCount:SetWidth(100) + --frame.killCount:SetHeight(24) + frame.killCount:SetPoint("RIGHT", frame, "RIGHT", -68, 0) + frame.killCount:SetJustifyH("RIGHT") + frame.killCount:SetText("0") + + ---@diagnostic disable-next-line: inject-field + frame.closeButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") + frame.closeButton:SetWidth(60) + frame.closeButton:SetHeight(24) + frame.closeButton:SetPoint("RIGHT", frame, "RIGHT", -3, 0) + frame.closeButton:SetText("Close") + frame.closeButton:SetScript("OnClick", function() I:Hide() end) +end + +function I:Show() + if not frame then SetupFrame() end + self.Kills = 0 + frame.killCount:SetText(tostring(self.Kills)) + frame:Show() + self.Active = true +end + +function I:Hide() + frame:Hide() + self.Kills = 0 + frame.killCount:SetText(tostring(self.Kills)) + self.Active = false +end + +function I:Toggle() + if frame and frame:IsShown() then + self:Hide() + else + self:Show() + end +end + +function I:AddKill() + self.Kills = self.Kills + 1 + frame.killCount:SetText(tostring(self.Kills)) +end diff --git a/KillTrack.lua b/KillTrack.lua index 786314b..6f22dc0 100644 --- a/KillTrack.lua +++ b/KillTrack.lua @@ -1,889 +1,889 @@ ---[[ - * Copyright (c) 2011-2020 by Adam Hellberg. - * - * This file is part of KillTrack. - * - * KillTrack is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KillTrack is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KillTrack. If not, see . ---]] - ----@type string -local NAME = ... - ----@class KillTrack ----@field PlayerName string ----@field PlayerGUID string? -local KT = select(2, ...) - ----@class KillTrackMobData ----@field Kills integer ----@field Name string ----@field LastKillAt integer? ----@field AchievCount integer ----@field Exp integer? - ----@class KillTrackCharMobData ----@field Kills integer ----@field Name string ----@field LastKillAt integer? - -_G[NAME] = KT - --- Upvalue some functions used in CLEU -local IsGUIDInGroup = IsGUIDInGroup -local UnitGUID = UnitGUID -local UnitIsTapDenied = UnitIsTapDenied -local UnitTokenFromGUID = UnitTokenFromGUID -local CombatLogGetCurrentEventInfo = CombatLogGetCurrentEventInfo -local GetServerTime = GetServerTime - -local NO_NAME = "" - -KT.Name = NAME -KT.Version = C_AddOns.GetAddOnMetadata(NAME, "Version") -KT.Events = {} - ----@class KillTrackImmediatePosition ----@field POINT string? ----@field RELATIVE string? ----@field X number? ----@field Y number? - ----@class KillTrackGlobal ----@field LOAD_MESSAGE boolean ----@field PRINTKILLS boolean ----@field PRINTNEW boolean ----@field ACHIEV_THRESHOLD integer ----@field COUNT_GROUP boolean ----@field SHOW_EXP boolean ----@field MOBS { [integer]: KillTrackMobData } ----@field IMMEDIATE { POSITION: KillTrackImmediatePosition, THRESHOLD: integer, FILTER: string? } ----@field BROKER { SHORT_TEXT: boolean, MINIMAP: { hide: boolean } } ----@field DISABLE_DUNGEONS boolean ----@field DISABLE_RAIDS boolean ----@field TOOLTIP boolean ----@field DATETIME_FORMAT string -KT.Global = {} - ----@class KillTrackCharGlobal ----@field MOBS { [integer]: KillTrackCharMobData } -KT.CharGlobal = {} - ----@class KillTrackTemp ----@field Threshold integer? ----@field DeleteId integer? -KT.Temp = {} - ----@enum KillTrackMobSortMode -KT.Sort = { - Desc = 0, - Asc = 1, - CharDesc = 2, - CharAsc = 3, - AlphaD = 4, - AlphaA = 5, - IdDesc = 6, - IdAsc = 7 -} -KT.Session = { - Count = 0, - ---@type { [string]: integer? } - Kills = {} -} -KT.Messages = { - Announce = "[KillTrack] Session Length: %s. Session Kills: %d. Kills Per Minute: %.2f." -} - -KT.Defaults = { - DateTimeFormat = "%Y-%m-%d %H:%M:%S" -} - ----@type KillTrackExpTracker -local ET - -local KTT = KT.Tools - --- Upvalue as it's used in CLEU -local GUIDToID = KTT.GUIDToID - ----@type { [string]: string? } -local FirstDamage = {} -- Tracks first damage to a mob registered by CLEU - ----@type { [string]: string? } -local LastDamage = {} -- Tracks whoever did the most recent damage to a mob - ----@type { [string]: boolean? } -local HasPlayerDamage = {} -- Tracks whether the player (or pet) has dealt damage to a mob - ----@type { [string]: boolean? } -local HasGroupDamage = {} -- Tracks whether a group member has dealt damage to a mob - ----@type { [string]: boolean? } -local DamageValid = {} -- Determines if mob is tapped by player/group - -local combat_log_damage_events = {} -do - local prefixes = { "SWING", "RANGE", "SPELL", "SPELL_PERIODIC", "SPELL_BUILDING" } - local suffixes = { "DAMAGE", "DRAIN", "LEECH", "INSTAKILL" } - for _, prefix in pairs(prefixes) do - for _, suffix in pairs(suffixes) do - combat_log_damage_events[prefix .. "_" .. suffix] = true - end - end -end - -if KT.Version == "@" .. "project-version" .. "@" then - KT.Version = "Development" - KT.Debug = true -end - -if not UnitTokenFromGUID then - local units = { - "player", - "vehicle", - "pet", - "party1", "party2", "party3", "party4", - "partypet1", "partypet2", "partypet3", "partypet4" - } - -- Multiple loops to get the same ordering as mainline API - for i = 1, 40 do - units[#units + 1] = "raid" .. i - end - for i = 1, 40 do - units[#units + 1] = "raidpet" .. i - end - for i = 1, 40 do - units[#units + 1] = "nameplate" .. i - end - for i = 1, 5 do - units[#units + 1] = "arena" .. i - end - for i = 1, 5 do - units[#units + 1] = "arenapet" .. i - end - for i = 1, 8 do - units[#units + 1] = "boss" .. i - end - units[#units + 1] = "target" - units[#units + 1] = "focus" - units[#units + 1] = "npc" - units[#units + 1] = "mouseover" - units[#units + 1] = "softenemy" - units[#units + 1] = "softfriend" - units[#units + 1] = "softinteract" - - UnitTokenFromGUID = function(guid) - for _, unit in ipairs(units) do - if UnitGUID(unit) == guid then - return unit - end - end - return nil - end -end - ----@param _ Frame ----@param event string ----@param ... any -function KT:OnEvent(_, event, ...) - if self.Events[event] then - self.Events[event](self, ...) - end -end - ----@param self KillTrack ----@param name string -function KT.Events.ADDON_LOADED(self, name) - if name ~= NAME then return end - ET = KT.ExpTracker - if type(_G["KILLTRACK"]) ~= "table" then - _G["KILLTRACK"] = {} - end - self.Global = _G["KILLTRACK"] - if type(self.Global.LOAD_MESSAGE) ~= "boolean" then - self.Global.LOAD_MESSAGE = false - end - if type(self.Global.PRINTKILLS) ~= "boolean" then - self.Global.PRINTKILLS = false - end - if type(self.Global.PRINTNEW) ~= "boolean" then - self.Global.PRINTNEW = false - end - if type(self.Global.ACHIEV_THRESHOLD) ~= "number" then - self.Global.ACHIEV_THRESHOLD = 1000 - end - if type(self.Global.COUNT_GROUP) ~= "boolean" then - self.Global.COUNT_GROUP = false - end - if type(self.Global.SHOW_EXP) ~= "boolean" then - self.Global.SHOW_EXP = false - end - if type(self.Global.MOBS) ~= "table" then - self.Global.MOBS = {} - end - if type(self.Global.IMMEDIATE) ~= "table" then - self.Global.IMMEDIATE = {} - end - if type(self.Global.IMMEDIATE.POSITION) ~= "table" then - self.Global.IMMEDIATE.POSITION = {} - end - if type(self.Global.IMMEDIATE.THRESHOLD) ~= "number" then - self.Global.IMMEDIATE.THRESHOLD = 0 - end - if type(self.Global.BROKER) ~= "table" then - self.Global.BROKER = {} - end - if type(self.Global.BROKER.SHORT_TEXT) ~= "boolean" then - self.Global.BROKER.SHORT_TEXT = false - end - if type(self.Global.BROKER.MINIMAP) ~= "table" then - self.Global.BROKER.MINIMAP = {} - end - if type(self.Global.DISABLE_DUNGEONS) ~= "boolean" then - self.Global.DISABLE_DUNGEONS = false - end - if type(self.Global.DISABLE_RAIDS) ~= "boolean" then - self.Global.DISABLE_RAIDS = false - end - if type(self.Global.TOOLTIP) ~= "boolean" then - self.Global.TOOLTIP = true - end - if type(self.Global.DATETIME_FORMAT) ~= "string" then - self.Global.DATETIME_FORMAT = self.Defaults.DateTimeFormat - end - if type(_G["KILLTRACK_CHAR"]) ~= "table" then - _G["KILLTRACK_CHAR"] = {} - end - self.CharGlobal = _G["KILLTRACK_CHAR"] - if type(self.CharGlobal.MOBS) ~= "table" then - self.CharGlobal.MOBS = {} - end - self.PlayerName = UnitName("player") - self.PlayerGUID = UnitGUID("player") - - if self.Global.LOAD_MESSAGE then - self:Msg("AddOn Loaded!") - end - - self.Session.Start = time() - self.Broker:OnLoad() -end - ----@param self KillTrack -function KT.Events.COMBAT_LOG_EVENT_UNFILTERED(self) - local _, event, _, s_guid, _, _, _, d_guid, d_name, _, _ = CombatLogGetCurrentEventInfo() - if combat_log_damage_events[event] then - if FirstDamage[d_guid] == nil then - -- s_guid is (probably) the player who first damaged this mob and probably has the tag - FirstDamage[d_guid] = s_guid - end - - LastDamage[d_guid] = s_guid - - if s_guid == self.PlayerGUID or s_guid == UnitGUID("pet") then - HasPlayerDamage[d_guid] = true - elseif self:IsInGroup(s_guid) then - HasGroupDamage[d_guid] = true - end - - if not DamageValid[d_guid] then - -- if DamageValid returns true for a GUID, we can tell with 100% certainty that it's valid - -- But this relies on one of the valid unit names currently being the damaged mob - - local d_unit = UnitTokenFromGUID(d_guid) - - if not d_unit then return end - - DamageValid[d_guid] = not UnitIsTapDenied(d_unit) - end - - return - end - - if event ~= "UNIT_DIED" then return end - - -- Perform solo/group checks - local d_id = GUIDToID(d_guid) - local firstDamage = FirstDamage[d_guid] - local lastDamage = LastDamage[d_guid] - local damageValid = DamageValid[d_guid] - local hasPlayerDamage = HasPlayerDamage[d_guid] - local hasGroupDamage = HasGroupDamage[d_guid] - FirstDamage[d_guid] = nil - LastDamage[d_guid] = nil - DamageValid[d_guid] = nil - HasPlayerDamage[d_guid] = nil - HasGroupDamage[d_guid] = nil - - -- If we can't identify the mob there's no point continuing - if d_id == nil or d_id == 0 then return end - - local petGUID = UnitGUID("pet") - local firstByPlayer = firstDamage == self.PlayerGUID or firstDamage == petGUID - local firstByGroup = self:IsInGroup(firstDamage) - local lastByPlayer = lastDamage == self.PlayerGUID or lastDamage == petGUID - - -- The checks when DamageValid is non-false are not 100% failsafe - -- Scenario: You deal the killing blow to an already tapped mob <- Would count as kill with current code - - -- If damageValid is false, it means tap is denied on the mob and there is no way the kill would count - if damageValid == false then return end - - -- If neither the player not group was involved in the battle, we don't count the kill - if not hasPlayerDamage and not hasGroupDamage then return end - - if damageValid == nil and not firstByPlayer and not firstByGroup then - -- If we couldn't get a proper tapped status and the first recorded damage was not by player or group, - -- we can't be sure the kill was valid, so ignore it - return - end - - if not lastByPlayer and not self.Global.COUNT_GROUP then - return -- Player or player's pet did not deal the killing blow and addon only tracks player kills - end - - self:AddKill(d_id, d_name) - if self.Timer:IsRunning() then - self.Timer:SetData("Kills", self.Timer:GetData("Kills", true) + 1) - end -end - ----@param _ KillTrack ----@param message string -function KT.Events.CHAT_MSG_COMBAT_XP_GAIN(_, message) - ET:CheckMessage(message) -end - ----@param self KillTrack ----@param size integer -function KT.Events.ENCOUNTER_START(self, _, _, _, size) - if (self.Global.DISABLE_DUNGEONS and size == 5) or (self.Global.DISABLE_RAIDS and size > 5) then - self.Frame:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED") - self.Frame:UnregisterEvent("UPDATE_MOUSEOVER_UNIT") - self.Frame:UnregisterEvent("CHAT_MSG_COMBAT_XP_GAIN") - end -end - ----@param self KillTrack ----@param size integer -function KT.Events.ENCOUNTER_END(self, _, _, _, size) - if (self.Global.DISABLE_DUNGEONS and size == 5) or (self.Global.DISABLE_RAIDS and size > 5) then - self.Frame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED") - self.Frame:RegisterEvent("UPDATE_MOUSEOVER_UNIT") - self.Frame:RegisterEvent("CHAT_MSG_COMBAT_XP_GAIN") - end -end - ----@param self GameTooltip -local function tooltip_enhancer(self) - if not KT.Global.TOOLTIP then return end - local _, unit = self:GetUnit() - if not unit then return end - if UnitIsPlayer(unit) then return end - local id = KTT.GUIDToID(UnitGUID(unit)) - if not id then return end - if UnitCanAttack("player", unit) then - local mob, charMob = KT:InitMob(id, UnitName(unit)) - local gKills, cKills = mob.Kills, charMob.Kills --self:GetKills(id) - local exp = mob.Exp - self:AddLine(("Killed %d (%d) times."):format(cKills, gKills), 1, 1, 1) - if KT.Global.SHOW_EXP and exp then - local toLevel = exp > 0 and math.ceil((UnitXPMax("player") - UnitXP("player")) / exp) or "N/A" - self:AddLine(("EXP: %d (%s kills to level)"):format(exp, toLevel), 1, 1, 1) - end - end - if KT.Debug then - self:AddLine(("ID = %d"):format(id)) - end - self:Show() -end - -if WOW_PROJECT_ID == WOW_PROJECT_MAINLINE then - TooltipDataProcessor.AddTooltipPostCall(Enum.TooltipDataType.Unit, tooltip_enhancer) -else - GameTooltip:HookScript("OnTooltipSetUnit", tooltip_enhancer) -end - -function KT:ToggleLoadMessage() - self.Global.LOAD_MESSAGE = not self.Global.LOAD_MESSAGE - if self.Global.LOAD_MESSAGE then - self:Msg("Now showing message on AddOn load") - else - self:Msg("No longer showing message on AddOn load") - end -end - -function KT:ToggleExp() - self.Global.SHOW_EXP = not self.Global.SHOW_EXP - if self.Global.SHOW_EXP then - self:Msg("Now showing EXP on tooltips!") - else - self:Msg("No longer showing EXP on tooltips.") - end -end - -function KT:ToggleDebug() - self.Debug = not self.Debug - if self.Debug then - self:Msg("Debug enabled!") - else - self:Msg("Debug disabled!") - end -end - ----@param guid string? ----@return boolean -function KT:IsInGroup(guid) - if not guid or guid == "" then return false end - if guid == self.PlayerName or guid == self.PlayerGUID then return true end - return IsGUIDInGroup(guid) -end - ----@param threshold integer -function KT:SetThreshold(threshold) - if type(threshold) ~= "number" then - error("KillTrack.SetThreshold: Argument #1 (threshold) must be of type 'number'") - end - self.Global.ACHIEV_THRESHOLD = threshold - if threshold > 0 then - self:ResetAchievCount() - self:Msg(("New kill notice (achievement) threshold set to %d."):format(threshold)) - else - self:Msg("Kill notices have been disabled (set threshold to a value greater than 0 to re-enable).") - end -end - ----@param threshold integer -function KT:SetImmediateThreshold(threshold) - if type(threshold) ~= "number" then - error("KillTrack.SetImmediateThreshold: Argument #1 (threshold) must be of type 'number'") - end - self.Global.IMMEDIATE.THRESHOLD = threshold - if threshold > 0 then - self:Msg(("New immediate threshold set to %d."):format(threshold)) - else - self:Msg("Immediate threshold disabled.") - end -end - ----@param filter string -function KT:SetImmediateFilter(filter) - if type(filter) ~= "string" then - error("KillTrack.SetImmediateFilter: Argument #1 (filter) must be of type 'string'") - end - self.Global.IMMEDIATE.FILTER = filter - self:Msg("New immediate filter set to: " .. filter) -end - -function KT:ClearImmediateFilter() - self.Global.IMMEDIATE.FILTER = nil - KT:Msg("Immediate filter cleared!") -end - -function KT:ToggleCountMode() - self.Global.COUNT_GROUP = not self.Global.COUNT_GROUP - if self.Global.COUNT_GROUP then - self:Msg("Now counting kills for every player in the group (party/raid)!") - else - self:Msg("Now counting your own killing blows ONLY.") - end -end - ----@param id integer ----@param name string? ----@return KillTrackMobData ----@return KillTrackCharMobData -function KT:InitMob(id, name) - name = name or NO_NAME - - if type(self.Global.MOBS[id]) ~= "table" then - self.Global.MOBS[id] = { Name = name, Kills = 0, AchievCount = 0 } - if self.Global.PRINTNEW then - self:Msg(("Created new entry for %q"):format(name)) - end - elseif self.Global.MOBS[id].Name ~= name then - self.Global.MOBS[id].Name = name - end - - if type(self.CharGlobal.MOBS[id]) ~= "table" then - self.CharGlobal.MOBS[id] = { Name = name, Kills = 0 } - if self.Global.PRINTNEW then - self:Msg(("Created new entry for %q on this character."):format(name)) - end - elseif self.CharGlobal.MOBS[id].Name ~= name then - self.CharGlobal.MOBS[id].Name = name - end - - return self.Global.MOBS[id], self.CharGlobal.MOBS[id] -end - ----@param id integer ----@param name string? -function KT:AddKill(id, name) - name = name or NO_NAME - local current_time = GetServerTime() - self:InitMob(id, name) - local globalMob = self.Global.MOBS[id] - local charMob = self.CharGlobal.MOBS[id] - globalMob.Kills = self.Global.MOBS[id].Kills + 1 - globalMob.LastKillAt = current_time - charMob.Kills = self.CharGlobal.MOBS[id].Kills + 1 - charMob.LastKillAt = current_time - if self.Global.PRINTKILLS then - local kills = self.Global.MOBS[id].Kills - local cKills = self.CharGlobal.MOBS[id].Kills - self:Msg(("Updated %q, new kill count: %d. Kill count on this character: %d"):format(name, kills, cKills)) - end - self:AddSessionKill(name) - if self.Immediate.Active then - local filter = self.Global.IMMEDIATE.FILTER - local filterPass = not filter or name:match(filter) - if filterPass then - self.Immediate:AddKill() - if self.Global.IMMEDIATE.THRESHOLD > 0 and self.Immediate.Kills % self.Global.IMMEDIATE.THRESHOLD == 0 then - PlaySound(SOUNDKIT.RAID_WARNING) - PlaySound(SOUNDKIT.PVP_THROUGH_QUEUE) - RaidNotice_AddMessage( - RaidWarningFrame, - ("%d KILLS!"):format(self.Immediate.Kills), - ChatTypeInfo.SYSTEM) - end - end - end - if self.Global.ACHIEV_THRESHOLD <= 0 then return end - if type(self.Global.MOBS[id].AchievCount) ~= "number" then - self.Global.MOBS[id].AchievCount = floor(self.Global.MOBS[id].Kills / self.Global.ACHIEV_THRESHOLD) - if self.Global.MOBS[id].AchievCount >= 1 then - self:KillAlert(self.Global.MOBS[id]) - end - else - local achievCount = self.Global.MOBS[id].AchievCount - self.Global.MOBS[id].AchievCount = floor(self.Global.MOBS[id].Kills / self.Global.ACHIEV_THRESHOLD) - if self.Global.MOBS[id].AchievCount > achievCount then - self:KillAlert(self.Global.MOBS[id]) - end - end -end - ----@param id integer ----@param name string? ----@param globalCount integer ----@param charCount integer -function KT:SetKills(id, name, globalCount, charCount) - if type(id) ~= "number" then - error("'id' argument must be a number") - end - - if type(globalCount) ~= "number" then - error("'globalCount' argument must be a number") - end - - if type(charCount) ~= "number" then - error("'charCount' argument must be a number") - end - - name = name or NO_NAME - - self:InitMob(id, name) - self.Global.MOBS[id].Kills = globalCount - self.CharGlobal.MOBS[id].Kills = charCount - - self:Msg(("Updated %q to %d global and %d character kills"):format(name, globalCount, charCount)) -end - ----@param name string -function KT:AddSessionKill(name) - if self.Session.Kills[name] then - self.Session.Kills[name] = self.Session.Kills[name] + 1 - else - self.Session.Kills[name] = 1 - end - self.Session.Count = self.Session.Count + 1 -end - ----@param name string ----@param exp integer|string -function KT:SetExp(name, exp) - for _, mob in pairs(self.Global.MOBS) do - if mob.Name == name then mob.Exp = tonumber(exp) end - end -end - ----@param max integer|string? ----@return { Name: string, Kills: integer }[] -function KT:GetSortedSessionKills(max) - max = tonumber(max) or 3 - local t = {} - for k,v in pairs(self.Session.Kills) do - t[#t + 1] = {Name = k, Kills = v} - end - table.sort(t, function(a, b) return a.Kills > b.Kills end) - -- Trim table to only contain 3 entries - local trimmed = {} - local c = 0 - for i,v in ipairs(t) do - trimmed[i] = v - c = c + 1 - if c >= max then break end - end - return trimmed -end - -function KT:ResetSession() - wipe(self.Session.Kills) - self.Session.Count = 0 - self.Session.Start = time() -end - ----@param id integer ----@return integer globalKills ----@return integer charKills -function KT:GetKills(id) - local gKills, cKills = 0, 0 - local mob = self.Global.MOBS[id] - if type(mob) == "table" then - gKills = mob.Kills - local cMob = self.CharGlobal.MOBS[id] - if type(cMob) == "table" then - cKills = cMob.Kills - end - end - return gKills, cKills -end - ----@return integer -function KT:GetTotalKills() - local count = 0 - for _, mob in pairs(self.Global.MOBS) do - count = count + mob.Kills - end - return count -end - ----@return integer killsPerSecond ----@return integer killsPerMinute ----@return integer killsPerHour ----@return integer killsThisSession -function KT:GetSessionStats() - if not self.Session.Start then return 0, 0, 0, 0 end - local now = time() - local session = now - self.Session.Start - local kps = session == 0 and 0 or self.Session.Count / session - local kpm = kps * 60 - local kph = kpm * 60 - return kps, kpm, kph, session -end - ----@param identifier string|integer? -function KT:PrintKills(identifier) - local found = false - local name = NO_NAME - local gKills = 0 - local cKills = 0 - local lastKillAt = nil - if type(identifier) ~= "string" and type(identifier) ~= "number" then identifier = NO_NAME end - for k,v in pairs(self.Global.MOBS) do - if type(v) == "table" and (tostring(k) == tostring(identifier) or v.Name == identifier) then - name = v.Name - gKills = v.Kills - if type(v.LastKillAt) == "number" then - lastKillAt = KTT:FormatDateTime(v.LastKillAt) - end - if self.CharGlobal.MOBS[k] then - cKills = self.CharGlobal.MOBS[k].Kills - end - found = true - end - end - if found then - self:Msg(("You have killed %q %d times in total, %d times on this character"):format(name, gKills, cKills)) - if lastKillAt then - self:Msg(("Last killed at %s"):format(lastKillAt)) - end - else - if UnitExists("target") and not UnitIsPlayer("target") then - identifier = UnitName("target") - end - self:Msg(("Unable to find %q in mob database."):format(tostring(identifier))) - end -end - ----@param target string -function KT:Announce(target) - if target == "GROUP" then - target = ((IsInRaid() and "RAID") or (IsInGroup() and "PARTY")) or "SAY" - end - local _, kpm, _, length = self:GetSessionStats() - local msg = self.Messages.Announce:format(KTT:FormatSeconds(length), self.Session.Count, kpm) - SendChatMessage(msg, target) -end - ----@param msg string -function KT:Msg(msg) - DEFAULT_CHAT_FRAME:AddMessage("\124cff00FF00[KillTrack]\124r " .. msg) -end - ----@param msg string -function KT:DebugMsg(msg) - if not self.Debug then return end - self:Msg("[DEBUG] " .. msg) -end - ----@param mob KillTrackMobData -function KT:KillAlert(mob) - local data = { - Text = ("%d kills on %s!"):format(mob.Kills, mob.Name), - Title = "Kill Record!", - bTitle = "Congratulations!", - Icon = "Interface\\Icons\\ABILITY_Deathwing_Bloodcorruption_Death", - FrameStyle = "GuildAchievement" - } - if C_AddOns.IsAddOnLoaded("Glamour") then - if not _G["GlamourShowAlert"] then - self:Msg("ERROR: GlamourShowAlert == nil! Notify AddOn developer.") - return - end - _G.GlamourShowAlert(500, data) - else - RaidNotice_AddMessage(RaidBossEmoteFrame, data.Text, ChatTypeInfo.SYSTEM) - RaidNotice_AddMessage(RaidBossEmoteFrame, data.Text, ChatTypeInfo.SYSTEM) - end - self:Msg(data.Text) -end - ----@param id integer|string ----@return KillTrackMobData|false ----@return KillTrackCharMobData|nil -function KT:GetMob(id) - for k,v in pairs(self.Global.MOBS) do - if type(v) == "table" and (tostring(k) == tostring(id) or v.Name == id) then - return v, self.CharGlobal.MOBS[k] - end - end - return false, nil -end - ----@param mode KillTrackMobSortMode|integer? ----@param filter string? ----@param caseSensitive boolean|nil ----@return { Id: integer, Name: string, gKills: integer, cKills: integer }[] -function KT:GetSortedMobTable(mode, filter, caseSensitive) - if not tonumber(mode) then mode = self.Sort.Desc end - if mode < 0 or mode > 7 then mode = self.Sort.Desc end - if filter and filter == "" then filter = nil end - local t = {} - for k,v in pairs(self.Global.MOBS) do - assert(type(v) == "table", "Unexpected mob entry type in db: " .. type(v) .. ". Expected table") - local matches = nil - if filter then - local name = caseSensitive and v.Name or v.Name:lower() - filter = caseSensitive and filter or filter:lower() - local status, result = pcall(string.match, name, filter) - matches = status and result - end - if matches or not filter then - local cKills = 0 - if self.CharGlobal.MOBS[k] and type(self.CharGlobal.MOBS[k]) == "table" then - cKills = self.CharGlobal.MOBS[k].Kills - end - local entry = {Id = k, Name = v.Name, gKills = v.Kills, cKills = cKills} - table.insert(t, entry) - end - end - local function compare(a, b) - if mode == self.Sort.Asc then - return a.gKills < b.gKills - elseif mode == self.Sort.CharDesc then - return a.cKills > b.cKills - elseif mode == self.Sort.CharAsc then - return a.cKills < b.cKills - elseif mode == self.Sort.AlphaD then - return a.Name > b.Name - elseif mode == self.Sort.AlphaA then - return a.Name < b.Name - elseif mode == self.Sort.IdDesc then - return a.Id > b.Id - elseif mode == self.Sort.IdAsc then - return a.Id < b.Id - else - return a.gKills > b.gKills -- Descending - end - end - table.sort(t, compare) - return t -end - ----@param id integer|string ----@param charOnly boolean? -function KT:Delete(id, charOnly) - id = tonumber(id) --[[@as integer]] - if not id then error(("Expected 'id' param to be number, got %s."):format(type(id))) end - local found = false - local name - if self.Global.MOBS[id] then - name = self.Global.MOBS[id].Name - if not charOnly then self.Global.MOBS[id] = nil end - if self.CharGlobal.MOBS[id] then - self.CharGlobal.MOBS[id] = nil - end - found = true - end - if found then - self:Msg(("Deleted %q (%d) from database."):format(name, id)) - StaticPopup_Show("KILLTRACK_FINISH", 1) - else - self:Msg(("ID: %d was not found in the database."):format(id)) - end -end - ----@param threshold integer -function KT:Purge(threshold) - local count = 0 - for k,v in pairs(self.Global.MOBS) do - if type(v) == "table" and v.Kills < threshold then - self.Global.MOBS[k] = nil - count = count + 1 - end - end - for k,v in pairs(self.CharGlobal.MOBS) do - if type(v) == "table" and v.Kills < threshold then - self.CharGlobal.MOBS[k] = nil - count = count + 1 - end - end - self:Msg(("Purged %d entries with a kill count below %d"):format(count, threshold)) - self.Temp.Threshold = nil - StaticPopup_Show("KILLTRACK_FINISH", tostring(count)) -end - -function KT:Reset() - local count = #self.Global.MOBS + #self.CharGlobal.MOBS - wipe(self.Global.MOBS) - wipe(self.CharGlobal.MOBS) - self:Msg(("%d mob entries have been removed!"):format(count)) - StaticPopup_Show("KILLTRACK_FINISH", tostring(count)) -end - -function KT:ResetAchievCount() - for _,v in pairs(self.Global.MOBS) do - v.AchievCount = floor(v.Kills / self.Global.ACHIEV_THRESHOLD) - end -end - -KT.Frame = CreateFrame("Frame") - -for k,_ in pairs(KT.Events) do - KT.Frame:RegisterEvent(k) -end - -KT.Frame:SetScript("OnEvent", function(_, event, ...) KT:OnEvent(_, event, ...) end) +--[[ + * Copyright (c) 2011-2020 by Adam Hellberg. + * + * This file is part of KillTrack. + * + * KillTrack is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KillTrack is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KillTrack. If not, see . +--]] + +---@type string +local NAME = ... + +---@class KillTrack +---@field PlayerName string +---@field PlayerGUID string? +local KT = select(2, ...) + +---@class KillTrackMobData +---@field Kills integer +---@field Name string +---@field LastKillAt integer? +---@field AchievCount integer +---@field Exp integer? + +---@class KillTrackCharMobData +---@field Kills integer +---@field Name string +---@field LastKillAt integer? + +_G[NAME] = KT + +-- Upvalue some functions used in CLEU +local IsGUIDInGroup = IsGUIDInGroup +local UnitGUID = UnitGUID +local UnitIsTapDenied = UnitIsTapDenied +local UnitTokenFromGUID = UnitTokenFromGUID +local CombatLogGetCurrentEventInfo = CombatLogGetCurrentEventInfo +local GetServerTime = GetServerTime + +local NO_NAME = "" + +KT.Name = NAME +KT.Version = C_AddOns.GetAddOnMetadata(NAME, "Version") +KT.Events = {} + +---@class KillTrackImmediatePosition +---@field POINT string? +---@field RELATIVE string? +---@field X number? +---@field Y number? + +---@class KillTrackGlobal +---@field LOAD_MESSAGE boolean +---@field PRINTKILLS boolean +---@field PRINTNEW boolean +---@field ACHIEV_THRESHOLD integer +---@field COUNT_GROUP boolean +---@field SHOW_EXP boolean +---@field MOBS { [integer]: KillTrackMobData } +---@field IMMEDIATE { POSITION: KillTrackImmediatePosition, THRESHOLD: integer, FILTER: string? } +---@field BROKER { SHORT_TEXT: boolean, MINIMAP: { hide: boolean } } +---@field DISABLE_DUNGEONS boolean +---@field DISABLE_RAIDS boolean +---@field TOOLTIP boolean +---@field DATETIME_FORMAT string +KT.Global = {} + +---@class KillTrackCharGlobal +---@field MOBS { [integer]: KillTrackCharMobData } +KT.CharGlobal = {} + +---@class KillTrackTemp +---@field Threshold integer? +---@field DeleteId integer? +KT.Temp = {} + +---@enum KillTrackMobSortMode +KT.Sort = { + Desc = 0, + Asc = 1, + CharDesc = 2, + CharAsc = 3, + AlphaD = 4, + AlphaA = 5, + IdDesc = 6, + IdAsc = 7 +} +KT.Session = { + Count = 0, + ---@type { [string]: integer? } + Kills = {} +} +KT.Messages = { + Announce = "[KillTrack] Session Length: %s. Session Kills: %d. Kills Per Minute: %.2f." +} + +KT.Defaults = { + DateTimeFormat = "%Y-%m-%d %H:%M:%S" +} + +---@type KillTrackExpTracker +local ET + +local KTT = KT.Tools + +-- Upvalue as it's used in CLEU +local GUIDToID = KTT.GUIDToID + +---@type { [string]: string? } +local FirstDamage = {} -- Tracks first damage to a mob registered by CLEU + +---@type { [string]: string? } +local LastDamage = {} -- Tracks whoever did the most recent damage to a mob + +---@type { [string]: boolean? } +local HasPlayerDamage = {} -- Tracks whether the player (or pet) has dealt damage to a mob + +---@type { [string]: boolean? } +local HasGroupDamage = {} -- Tracks whether a group member has dealt damage to a mob + +---@type { [string]: boolean? } +local DamageValid = {} -- Determines if mob is tapped by player/group + +local combat_log_damage_events = {} +do + local prefixes = { "SWING", "RANGE", "SPELL", "SPELL_PERIODIC", "SPELL_BUILDING" } + local suffixes = { "DAMAGE", "DRAIN", "LEECH", "INSTAKILL" } + for _, prefix in pairs(prefixes) do + for _, suffix in pairs(suffixes) do + combat_log_damage_events[prefix .. "_" .. suffix] = true + end + end +end + +if KT.Version == "@" .. "project-version" .. "@" then + KT.Version = "Development" + KT.Debug = true +end + +if not UnitTokenFromGUID then + local units = { + "player", + "vehicle", + "pet", + "party1", "party2", "party3", "party4", + "partypet1", "partypet2", "partypet3", "partypet4" + } + -- Multiple loops to get the same ordering as mainline API + for i = 1, 40 do + units[#units + 1] = "raid" .. i + end + for i = 1, 40 do + units[#units + 1] = "raidpet" .. i + end + for i = 1, 40 do + units[#units + 1] = "nameplate" .. i + end + for i = 1, 5 do + units[#units + 1] = "arena" .. i + end + for i = 1, 5 do + units[#units + 1] = "arenapet" .. i + end + for i = 1, 8 do + units[#units + 1] = "boss" .. i + end + units[#units + 1] = "target" + units[#units + 1] = "focus" + units[#units + 1] = "npc" + units[#units + 1] = "mouseover" + units[#units + 1] = "softenemy" + units[#units + 1] = "softfriend" + units[#units + 1] = "softinteract" + + UnitTokenFromGUID = function(guid) + for _, unit in ipairs(units) do + if UnitGUID(unit) == guid then + return unit + end + end + return nil + end +end + +---@param _ Frame +---@param event string +---@param ... any +function KT:OnEvent(_, event, ...) + if self.Events[event] then + self.Events[event](self, ...) + end +end + +---@param self KillTrack +---@param name string +function KT.Events.ADDON_LOADED(self, name) + if name ~= NAME then return end + ET = KT.ExpTracker + if type(_G["KILLTRACK"]) ~= "table" then + _G["KILLTRACK"] = {} + end + self.Global = _G["KILLTRACK"] + if type(self.Global.LOAD_MESSAGE) ~= "boolean" then + self.Global.LOAD_MESSAGE = false + end + if type(self.Global.PRINTKILLS) ~= "boolean" then + self.Global.PRINTKILLS = false + end + if type(self.Global.PRINTNEW) ~= "boolean" then + self.Global.PRINTNEW = false + end + if type(self.Global.ACHIEV_THRESHOLD) ~= "number" then + self.Global.ACHIEV_THRESHOLD = 1000 + end + if type(self.Global.COUNT_GROUP) ~= "boolean" then + self.Global.COUNT_GROUP = false + end + if type(self.Global.SHOW_EXP) ~= "boolean" then + self.Global.SHOW_EXP = false + end + if type(self.Global.MOBS) ~= "table" then + self.Global.MOBS = {} + end + if type(self.Global.IMMEDIATE) ~= "table" then + self.Global.IMMEDIATE = {} + end + if type(self.Global.IMMEDIATE.POSITION) ~= "table" then + self.Global.IMMEDIATE.POSITION = {} + end + if type(self.Global.IMMEDIATE.THRESHOLD) ~= "number" then + self.Global.IMMEDIATE.THRESHOLD = 0 + end + if type(self.Global.BROKER) ~= "table" then + self.Global.BROKER = {} + end + if type(self.Global.BROKER.SHORT_TEXT) ~= "boolean" then + self.Global.BROKER.SHORT_TEXT = false + end + if type(self.Global.BROKER.MINIMAP) ~= "table" then + self.Global.BROKER.MINIMAP = {} + end + if type(self.Global.DISABLE_DUNGEONS) ~= "boolean" then + self.Global.DISABLE_DUNGEONS = false + end + if type(self.Global.DISABLE_RAIDS) ~= "boolean" then + self.Global.DISABLE_RAIDS = false + end + if type(self.Global.TOOLTIP) ~= "boolean" then + self.Global.TOOLTIP = true + end + if type(self.Global.DATETIME_FORMAT) ~= "string" then + self.Global.DATETIME_FORMAT = self.Defaults.DateTimeFormat + end + if type(_G["KILLTRACK_CHAR"]) ~= "table" then + _G["KILLTRACK_CHAR"] = {} + end + self.CharGlobal = _G["KILLTRACK_CHAR"] + if type(self.CharGlobal.MOBS) ~= "table" then + self.CharGlobal.MOBS = {} + end + self.PlayerName = UnitName("player") + self.PlayerGUID = UnitGUID("player") + + if self.Global.LOAD_MESSAGE then + self:Msg("AddOn Loaded!") + end + + self.Session.Start = time() + self.Broker:OnLoad() +end + +---@param self KillTrack +function KT.Events.COMBAT_LOG_EVENT_UNFILTERED(self) + local _, event, _, s_guid, _, _, _, d_guid, d_name, _, _ = CombatLogGetCurrentEventInfo() + if combat_log_damage_events[event] then + if FirstDamage[d_guid] == nil then + -- s_guid is (probably) the player who first damaged this mob and probably has the tag + FirstDamage[d_guid] = s_guid + end + + LastDamage[d_guid] = s_guid + + if s_guid == self.PlayerGUID or s_guid == UnitGUID("pet") then + HasPlayerDamage[d_guid] = true + elseif self:IsInGroup(s_guid) then + HasGroupDamage[d_guid] = true + end + + if not DamageValid[d_guid] then + -- if DamageValid returns true for a GUID, we can tell with 100% certainty that it's valid + -- But this relies on one of the valid unit names currently being the damaged mob + + local d_unit = UnitTokenFromGUID(d_guid) + + if not d_unit then return end + + DamageValid[d_guid] = not UnitIsTapDenied(d_unit) + end + + return + end + + if event ~= "UNIT_DIED" then return end + + -- Perform solo/group checks + local d_id = GUIDToID(d_guid) + local firstDamage = FirstDamage[d_guid] + local lastDamage = LastDamage[d_guid] + local damageValid = DamageValid[d_guid] + local hasPlayerDamage = HasPlayerDamage[d_guid] + local hasGroupDamage = HasGroupDamage[d_guid] + FirstDamage[d_guid] = nil + LastDamage[d_guid] = nil + DamageValid[d_guid] = nil + HasPlayerDamage[d_guid] = nil + HasGroupDamage[d_guid] = nil + + -- If we can't identify the mob there's no point continuing + if d_id == nil or d_id == 0 then return end + + local petGUID = UnitGUID("pet") + local firstByPlayer = firstDamage == self.PlayerGUID or firstDamage == petGUID + local firstByGroup = self:IsInGroup(firstDamage) + local lastByPlayer = lastDamage == self.PlayerGUID or lastDamage == petGUID + + -- The checks when DamageValid is non-false are not 100% failsafe + -- Scenario: You deal the killing blow to an already tapped mob <- Would count as kill with current code + + -- If damageValid is false, it means tap is denied on the mob and there is no way the kill would count + if damageValid == false then return end + + -- If neither the player not group was involved in the battle, we don't count the kill + if not hasPlayerDamage and not hasGroupDamage then return end + + if damageValid == nil and not firstByPlayer and not firstByGroup then + -- If we couldn't get a proper tapped status and the first recorded damage was not by player or group, + -- we can't be sure the kill was valid, so ignore it + return + end + + if not lastByPlayer and not self.Global.COUNT_GROUP then + return -- Player or player's pet did not deal the killing blow and addon only tracks player kills + end + + self:AddKill(d_id, d_name) + if self.Timer:IsRunning() then + self.Timer:SetData("Kills", self.Timer:GetData("Kills", true) + 1) + end +end + +---@param _ KillTrack +---@param message string +function KT.Events.CHAT_MSG_COMBAT_XP_GAIN(_, message) + ET:CheckMessage(message) +end + +---@param self KillTrack +---@param size integer +function KT.Events.ENCOUNTER_START(self, _, _, _, size) + if (self.Global.DISABLE_DUNGEONS and size == 5) or (self.Global.DISABLE_RAIDS and size > 5) then + self.Frame:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED") + self.Frame:UnregisterEvent("UPDATE_MOUSEOVER_UNIT") + self.Frame:UnregisterEvent("CHAT_MSG_COMBAT_XP_GAIN") + end +end + +---@param self KillTrack +---@param size integer +function KT.Events.ENCOUNTER_END(self, _, _, _, size) + if (self.Global.DISABLE_DUNGEONS and size == 5) or (self.Global.DISABLE_RAIDS and size > 5) then + self.Frame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED") + self.Frame:RegisterEvent("UPDATE_MOUSEOVER_UNIT") + self.Frame:RegisterEvent("CHAT_MSG_COMBAT_XP_GAIN") + end +end + +---@param self GameTooltip +local function tooltip_enhancer(self) + if not KT.Global.TOOLTIP then return end + local _, unit = self:GetUnit() + if not unit then return end + if UnitIsPlayer(unit) then return end + local id = KTT.GUIDToID(UnitGUID(unit)) + if not id then return end + if UnitCanAttack("player", unit) then + local mob, charMob = KT:InitMob(id, UnitName(unit)) + local gKills, cKills = mob.Kills, charMob.Kills --self:GetKills(id) + local exp = mob.Exp + self:AddLine(("Killed %d (%d) times."):format(cKills, gKills), 1, 1, 1) + if KT.Global.SHOW_EXP and exp then + local toLevel = exp > 0 and math.ceil((UnitXPMax("player") - UnitXP("player")) / exp) or "N/A" + self:AddLine(("EXP: %d (%s kills to level)"):format(exp, toLevel), 1, 1, 1) + end + end + if KT.Debug then + self:AddLine(("ID = %d"):format(id)) + end + self:Show() +end + +if WOW_PROJECT_ID == WOW_PROJECT_MAINLINE then + TooltipDataProcessor.AddTooltipPostCall(Enum.TooltipDataType.Unit, tooltip_enhancer) +else + GameTooltip:HookScript("OnTooltipSetUnit", tooltip_enhancer) +end + +function KT:ToggleLoadMessage() + self.Global.LOAD_MESSAGE = not self.Global.LOAD_MESSAGE + if self.Global.LOAD_MESSAGE then + self:Msg("Now showing message on AddOn load") + else + self:Msg("No longer showing message on AddOn load") + end +end + +function KT:ToggleExp() + self.Global.SHOW_EXP = not self.Global.SHOW_EXP + if self.Global.SHOW_EXP then + self:Msg("Now showing EXP on tooltips!") + else + self:Msg("No longer showing EXP on tooltips.") + end +end + +function KT:ToggleDebug() + self.Debug = not self.Debug + if self.Debug then + self:Msg("Debug enabled!") + else + self:Msg("Debug disabled!") + end +end + +---@param guid string? +---@return boolean +function KT:IsInGroup(guid) + if not guid or guid == "" then return false end + if guid == self.PlayerName or guid == self.PlayerGUID then return true end + return IsGUIDInGroup(guid) +end + +---@param threshold integer +function KT:SetThreshold(threshold) + if type(threshold) ~= "number" then + error("KillTrack.SetThreshold: Argument #1 (threshold) must be of type 'number'") + end + self.Global.ACHIEV_THRESHOLD = threshold + if threshold > 0 then + self:ResetAchievCount() + self:Msg(("New kill notice (achievement) threshold set to %d."):format(threshold)) + else + self:Msg("Kill notices have been disabled (set threshold to a value greater than 0 to re-enable).") + end +end + +---@param threshold integer +function KT:SetImmediateThreshold(threshold) + if type(threshold) ~= "number" then + error("KillTrack.SetImmediateThreshold: Argument #1 (threshold) must be of type 'number'") + end + self.Global.IMMEDIATE.THRESHOLD = threshold + if threshold > 0 then + self:Msg(("New immediate threshold set to %d."):format(threshold)) + else + self:Msg("Immediate threshold disabled.") + end +end + +---@param filter string +function KT:SetImmediateFilter(filter) + if type(filter) ~= "string" then + error("KillTrack.SetImmediateFilter: Argument #1 (filter) must be of type 'string'") + end + self.Global.IMMEDIATE.FILTER = filter + self:Msg("New immediate filter set to: " .. filter) +end + +function KT:ClearImmediateFilter() + self.Global.IMMEDIATE.FILTER = nil + KT:Msg("Immediate filter cleared!") +end + +function KT:ToggleCountMode() + self.Global.COUNT_GROUP = not self.Global.COUNT_GROUP + if self.Global.COUNT_GROUP then + self:Msg("Now counting kills for every player in the group (party/raid)!") + else + self:Msg("Now counting your own killing blows ONLY.") + end +end + +---@param id integer +---@param name string? +---@return KillTrackMobData +---@return KillTrackCharMobData +function KT:InitMob(id, name) + name = name or NO_NAME + + if type(self.Global.MOBS[id]) ~= "table" then + self.Global.MOBS[id] = { Name = name, Kills = 0, AchievCount = 0 } + if self.Global.PRINTNEW then + self:Msg(("Created new entry for %q"):format(name)) + end + elseif self.Global.MOBS[id].Name ~= name then + self.Global.MOBS[id].Name = name + end + + if type(self.CharGlobal.MOBS[id]) ~= "table" then + self.CharGlobal.MOBS[id] = { Name = name, Kills = 0 } + if self.Global.PRINTNEW then + self:Msg(("Created new entry for %q on this character."):format(name)) + end + elseif self.CharGlobal.MOBS[id].Name ~= name then + self.CharGlobal.MOBS[id].Name = name + end + + return self.Global.MOBS[id], self.CharGlobal.MOBS[id] +end + +---@param id integer +---@param name string? +function KT:AddKill(id, name) + name = name or NO_NAME + local current_time = GetServerTime() + self:InitMob(id, name) + local globalMob = self.Global.MOBS[id] + local charMob = self.CharGlobal.MOBS[id] + globalMob.Kills = self.Global.MOBS[id].Kills + 1 + globalMob.LastKillAt = current_time + charMob.Kills = self.CharGlobal.MOBS[id].Kills + 1 + charMob.LastKillAt = current_time + if self.Global.PRINTKILLS then + local kills = self.Global.MOBS[id].Kills + local cKills = self.CharGlobal.MOBS[id].Kills + self:Msg(("Updated %q, new kill count: %d. Kill count on this character: %d"):format(name, kills, cKills)) + end + self:AddSessionKill(name) + if self.Immediate.Active then + local filter = self.Global.IMMEDIATE.FILTER + local filterPass = not filter or name:match(filter) + if filterPass then + self.Immediate:AddKill() + if self.Global.IMMEDIATE.THRESHOLD > 0 and self.Immediate.Kills % self.Global.IMMEDIATE.THRESHOLD == 0 then + PlaySound(SOUNDKIT.RAID_WARNING) + PlaySound(SOUNDKIT.PVP_THROUGH_QUEUE) + RaidNotice_AddMessage( + RaidWarningFrame, + ("%d KILLS!"):format(self.Immediate.Kills), + ChatTypeInfo.SYSTEM) + end + end + end + if self.Global.ACHIEV_THRESHOLD <= 0 then return end + if type(self.Global.MOBS[id].AchievCount) ~= "number" then + self.Global.MOBS[id].AchievCount = floor(self.Global.MOBS[id].Kills / self.Global.ACHIEV_THRESHOLD) + if self.Global.MOBS[id].AchievCount >= 1 then + self:KillAlert(self.Global.MOBS[id]) + end + else + local achievCount = self.Global.MOBS[id].AchievCount + self.Global.MOBS[id].AchievCount = floor(self.Global.MOBS[id].Kills / self.Global.ACHIEV_THRESHOLD) + if self.Global.MOBS[id].AchievCount > achievCount then + self:KillAlert(self.Global.MOBS[id]) + end + end +end + +---@param id integer +---@param name string? +---@param globalCount integer +---@param charCount integer +function KT:SetKills(id, name, globalCount, charCount) + if type(id) ~= "number" then + error("'id' argument must be a number") + end + + if type(globalCount) ~= "number" then + error("'globalCount' argument must be a number") + end + + if type(charCount) ~= "number" then + error("'charCount' argument must be a number") + end + + name = name or NO_NAME + + self:InitMob(id, name) + self.Global.MOBS[id].Kills = globalCount + self.CharGlobal.MOBS[id].Kills = charCount + + self:Msg(("Updated %q to %d global and %d character kills"):format(name, globalCount, charCount)) +end + +---@param name string +function KT:AddSessionKill(name) + if self.Session.Kills[name] then + self.Session.Kills[name] = self.Session.Kills[name] + 1 + else + self.Session.Kills[name] = 1 + end + self.Session.Count = self.Session.Count + 1 +end + +---@param name string +---@param exp integer|string +function KT:SetExp(name, exp) + for _, mob in pairs(self.Global.MOBS) do + if mob.Name == name then mob.Exp = tonumber(exp) end + end +end + +---@param max integer|string? +---@return { Name: string, Kills: integer }[] +function KT:GetSortedSessionKills(max) + max = tonumber(max) or 3 + local t = {} + for k,v in pairs(self.Session.Kills) do + t[#t + 1] = {Name = k, Kills = v} + end + table.sort(t, function(a, b) return a.Kills > b.Kills end) + -- Trim table to only contain 3 entries + local trimmed = {} + local c = 0 + for i,v in ipairs(t) do + trimmed[i] = v + c = c + 1 + if c >= max then break end + end + return trimmed +end + +function KT:ResetSession() + wipe(self.Session.Kills) + self.Session.Count = 0 + self.Session.Start = time() +end + +---@param id integer +---@return integer globalKills +---@return integer charKills +function KT:GetKills(id) + local gKills, cKills = 0, 0 + local mob = self.Global.MOBS[id] + if type(mob) == "table" then + gKills = mob.Kills + local cMob = self.CharGlobal.MOBS[id] + if type(cMob) == "table" then + cKills = cMob.Kills + end + end + return gKills, cKills +end + +---@return integer +function KT:GetTotalKills() + local count = 0 + for _, mob in pairs(self.Global.MOBS) do + count = count + mob.Kills + end + return count +end + +---@return integer killsPerSecond +---@return integer killsPerMinute +---@return integer killsPerHour +---@return integer killsThisSession +function KT:GetSessionStats() + if not self.Session.Start then return 0, 0, 0, 0 end + local now = time() + local session = now - self.Session.Start + local kps = session == 0 and 0 or self.Session.Count / session + local kpm = kps * 60 + local kph = kpm * 60 + return kps, kpm, kph, session +end + +---@param identifier string|integer? +function KT:PrintKills(identifier) + local found = false + local name = NO_NAME + local gKills = 0 + local cKills = 0 + local lastKillAt = nil + if type(identifier) ~= "string" and type(identifier) ~= "number" then identifier = NO_NAME end + for k,v in pairs(self.Global.MOBS) do + if type(v) == "table" and (tostring(k) == tostring(identifier) or v.Name == identifier) then + name = v.Name + gKills = v.Kills + if type(v.LastKillAt) == "number" then + lastKillAt = KTT:FormatDateTime(v.LastKillAt) + end + if self.CharGlobal.MOBS[k] then + cKills = self.CharGlobal.MOBS[k].Kills + end + found = true + end + end + if found then + self:Msg(("You have killed %q %d times in total, %d times on this character"):format(name, gKills, cKills)) + if lastKillAt then + self:Msg(("Last killed at %s"):format(lastKillAt)) + end + else + if UnitExists("target") and not UnitIsPlayer("target") then + identifier = UnitName("target") + end + self:Msg(("Unable to find %q in mob database."):format(tostring(identifier))) + end +end + +---@param target string +function KT:Announce(target) + if target == "GROUP" then + target = ((IsInRaid() and "RAID") or (IsInGroup() and "PARTY")) or "SAY" + end + local _, kpm, _, length = self:GetSessionStats() + local msg = self.Messages.Announce:format(KTT:FormatSeconds(length), self.Session.Count, kpm) + SendChatMessage(msg, target) +end + +---@param msg string +function KT:Msg(msg) + DEFAULT_CHAT_FRAME:AddMessage("\124cff00FF00[KillTrack]\124r " .. msg) +end + +---@param msg string +function KT:DebugMsg(msg) + if not self.Debug then return end + self:Msg("[DEBUG] " .. msg) +end + +---@param mob KillTrackMobData +function KT:KillAlert(mob) + local data = { + Text = ("%d kills on %s!"):format(mob.Kills, mob.Name), + Title = "Kill Record!", + bTitle = "Congratulations!", + Icon = "Interface\\Icons\\ABILITY_Deathwing_Bloodcorruption_Death", + FrameStyle = "GuildAchievement" + } + if C_AddOns.IsAddOnLoaded("Glamour") then + if not _G["GlamourShowAlert"] then + self:Msg("ERROR: GlamourShowAlert == nil! Notify AddOn developer.") + return + end + _G.GlamourShowAlert(500, data) + else + RaidNotice_AddMessage(RaidBossEmoteFrame, data.Text, ChatTypeInfo.SYSTEM) + RaidNotice_AddMessage(RaidBossEmoteFrame, data.Text, ChatTypeInfo.SYSTEM) + end + self:Msg(data.Text) +end + +---@param id integer|string +---@return KillTrackMobData|false +---@return KillTrackCharMobData|nil +function KT:GetMob(id) + for k,v in pairs(self.Global.MOBS) do + if type(v) == "table" and (tostring(k) == tostring(id) or v.Name == id) then + return v, self.CharGlobal.MOBS[k] + end + end + return false, nil +end + +---@param mode KillTrackMobSortMode|integer? +---@param filter string? +---@param caseSensitive boolean|nil +---@return { Id: integer, Name: string, gKills: integer, cKills: integer }[] +function KT:GetSortedMobTable(mode, filter, caseSensitive) + if not tonumber(mode) then mode = self.Sort.Desc end + if mode < 0 or mode > 7 then mode = self.Sort.Desc end + if filter and filter == "" then filter = nil end + local t = {} + for k,v in pairs(self.Global.MOBS) do + assert(type(v) == "table", "Unexpected mob entry type in db: " .. type(v) .. ". Expected table") + local matches = nil + if filter then + local name = caseSensitive and v.Name or v.Name:lower() + filter = caseSensitive and filter or filter:lower() + local status, result = pcall(string.match, name, filter) + matches = status and result + end + if matches or not filter then + local cKills = 0 + if self.CharGlobal.MOBS[k] and type(self.CharGlobal.MOBS[k]) == "table" then + cKills = self.CharGlobal.MOBS[k].Kills + end + local entry = {Id = k, Name = v.Name, gKills = v.Kills, cKills = cKills} + table.insert(t, entry) + end + end + local function compare(a, b) + if mode == self.Sort.Asc then + return a.gKills < b.gKills + elseif mode == self.Sort.CharDesc then + return a.cKills > b.cKills + elseif mode == self.Sort.CharAsc then + return a.cKills < b.cKills + elseif mode == self.Sort.AlphaD then + return a.Name > b.Name + elseif mode == self.Sort.AlphaA then + return a.Name < b.Name + elseif mode == self.Sort.IdDesc then + return a.Id > b.Id + elseif mode == self.Sort.IdAsc then + return a.Id < b.Id + else + return a.gKills > b.gKills -- Descending + end + end + table.sort(t, compare) + return t +end + +---@param id integer|string +---@param charOnly boolean? +function KT:Delete(id, charOnly) + id = tonumber(id) --[[@as integer]] + if not id then error(("Expected 'id' param to be number, got %s."):format(type(id))) end + local found = false + local name + if self.Global.MOBS[id] then + name = self.Global.MOBS[id].Name + if not charOnly then self.Global.MOBS[id] = nil end + if self.CharGlobal.MOBS[id] then + self.CharGlobal.MOBS[id] = nil + end + found = true + end + if found then + self:Msg(("Deleted %q (%d) from database."):format(name, id)) + StaticPopup_Show("KILLTRACK_FINISH", 1) + else + self:Msg(("ID: %d was not found in the database."):format(id)) + end +end + +---@param threshold integer +function KT:Purge(threshold) + local count = 0 + for k,v in pairs(self.Global.MOBS) do + if type(v) == "table" and v.Kills < threshold then + self.Global.MOBS[k] = nil + count = count + 1 + end + end + for k,v in pairs(self.CharGlobal.MOBS) do + if type(v) == "table" and v.Kills < threshold then + self.CharGlobal.MOBS[k] = nil + count = count + 1 + end + end + self:Msg(("Purged %d entries with a kill count below %d"):format(count, threshold)) + self.Temp.Threshold = nil + StaticPopup_Show("KILLTRACK_FINISH", tostring(count)) +end + +function KT:Reset() + local count = #self.Global.MOBS + #self.CharGlobal.MOBS + wipe(self.Global.MOBS) + wipe(self.CharGlobal.MOBS) + self:Msg(("%d mob entries have been removed!"):format(count)) + StaticPopup_Show("KILLTRACK_FINISH", tostring(count)) +end + +function KT:ResetAchievCount() + for _,v in pairs(self.Global.MOBS) do + v.AchievCount = floor(v.Kills / self.Global.ACHIEV_THRESHOLD) + end +end + +KT.Frame = CreateFrame("Frame") + +for k,_ in pairs(KT.Events) do + KT.Frame:RegisterEvent(k) +end + +KT.Frame:SetScript("OnEvent", function(_, event, ...) KT:OnEvent(_, event, ...) end) diff --git a/MobList.lua b/MobList.lua index b805a97..29ae418 100644 --- a/MobList.lua +++ b/MobList.lua @@ -1,476 +1,476 @@ ---[[ - * Copyright (c) 2011-2020 by Adam Hellberg. - * - * This file is part of KillTrack. - * - * KillTrack is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KillTrack is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KillTrack. If not, see . ---]] - --- Beware of some possibly messy code in this file - ----@class KillTrack -local KT = select(2, ...) - ----@class KillTrackMobList -local ML = {} - -KT.MobList = ML -local KTT = KT.Tools - -local Sort = KT.Sort.Desc ----@type { Id: integer, Name: string, gKills: integer, cKills: integer }[] -local Mobs = nil -local LastFilter = nil -local LastOffset = 0 - --- Frame Constants -local FRAME_WIDTH = 600 -local FRAME_HEIGHT = 534 -local HEADER_HEIGHT = 24 -local HEADER_LEFT = 3 -local HEADER_TOP = -80 -local ROW_HEIGHT = 15 -local ROW_COUNT = 27 -local ROW_TEXT_PADDING = 5 -local ID_WIDTH = 100 -local NAME_WIDTH = 300 -local CHAR_WIDTH = 100 -local GLOBAL_WIDTH = 100 -local SCROLL_WIDTH = 27 -- Scrollbar width -local STATUS_TEXT = "Showing entries %d through %d out of %d total (%d hidden)" - ----@type table|BackdropTemplate|Frame -local frame = nil -local created = false - --- Frame helper functions - ----@param parent table|Frame ----@return Button -local function CreateHeader(parent) - local h = CreateFrame("Button", nil, parent) - h:SetHeight(HEADER_HEIGHT) - h:SetNormalFontObject("GameFontHighlightSmall") - - local bgl = h:CreateTexture(nil, "BACKGROUND") - bgl:SetTexture("Interface\\FriendsFrame\\WhoFrame-ColumnTabs") - bgl:SetWidth(5) - bgl:SetHeight(HEADER_HEIGHT) - bgl:SetPoint("TOPLEFT") - bgl:SetTexCoord(0, 0.07815, 0, 0.75) - - local bgr = h:CreateTexture(nil, "BACKGROUND") - bgr:SetTexture("Interface\\FriendsFrame\\WhoFrame-ColumnTabs") - bgr:SetWidth(5) - bgr:SetHeight(HEADER_HEIGHT) - bgr:SetPoint("TOPRIGHT") - bgr:SetTexCoord(0.90625, 0.96875, 0, 0.75) - - local bgm = h:CreateTexture(nil, "BACKGROUND") - bgm:SetTexture("Interface\\FriendsFrame\\WhoFrame-ColumnTabs") - bgm:SetHeight(HEADER_HEIGHT) - bgm:SetPoint("LEFT", bgl, "RIGHT") - bgm:SetPoint("RIGHT", bgr, "LEFT") - bgm:SetTexCoord(0.07815, 0.90625, 0, 0.75) - - local hl = h:CreateTexture() - h:SetHighlightTexture("Interface\\PaperDollInfoFrame\\UI-Character-Tab-Highlight", "ADD") - hl:SetPoint("TOPLEFT", bgl, "TOPLEFT", -2, 5) - hl:SetPoint("BOTTOMRIGHT", bgr, "BOTTOMRIGHT", 2, -7) - - return h -end - ----@param container table|Frame ----@param previous table|Frame ----@return Button -local function CreateRow(container, previous) - local row = CreateFrame("Button", nil, container) - row:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight", "ADD") - row:SetHeight(ROW_HEIGHT) - row:SetPoint("LEFT") - row:SetPoint("RIGHT") - row:SetPoint("TOPLEFT", previous, "BOTTOMLEFT", 0, 0) - - ---@diagnostic disable-next-line: inject-field - row.idField = row:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") - row.idField:SetHeight(ROW_HEIGHT) - row.idField:SetWidth(ID_WIDTH - ROW_TEXT_PADDING * 2) - row.idField:SetPoint("LEFT", row, "LEFT", ROW_TEXT_PADDING, 0) - row.idField:SetJustifyH("RIGHT") - - ---@diagnostic disable-next-line: inject-field - row.nameField = row:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") - row.nameField:SetHeight(ROW_HEIGHT) - row.nameField:SetWidth(NAME_WIDTH - SCROLL_WIDTH - ROW_TEXT_PADDING * 3) - row.nameField:SetPoint("LEFT", row.idField, "RIGHT", 2 * ROW_TEXT_PADDING, 0) - row.nameField:SetJustifyH("LEFT") - - ---@diagnostic disable-next-line: inject-field - row.charKillField = row:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") - row.charKillField:SetHeight(ROW_HEIGHT) - row.charKillField:SetWidth(CHAR_WIDTH - ROW_TEXT_PADDING * 2) - row.charKillField:SetPoint("LEFT", row.nameField, "RIGHT", 2 * ROW_TEXT_PADDING, 0) - row.charKillField:SetJustifyH("RIGHT") - - ---@diagnostic disable-next-line: inject-field - row.globalKillField = row:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") - row.globalKillField:SetHeight(ROW_HEIGHT) - row.globalKillField:SetWidth(GLOBAL_WIDTH - ROW_TEXT_PADDING * 2) - row.globalKillField:SetPoint("LEFT", row.charKillField, "RIGHT", 2 * ROW_TEXT_PADDING, 0) - row.globalKillField:SetJustifyH("RIGHT") - - row:SetScript("OnClick", function(s) - local id = tonumber(s.idField:GetText()) - if not id then return end - local name = s.nameField:GetText() - KT:ShowDelete(id, name) - end) - - row:SetScript("OnEnter", function(s) - local id = tonumber(s.idField:GetText()) - if not id then return end - local globalData = KT.Global.MOBS[id] - if not globalData then return end - local killTimestamp = globalData.LastKillAt - if not killTimestamp then return end - local lastKillAt = KTT:FormatDateTime(killTimestamp) - local tpString = ("Last killed at %s"):format(lastKillAt) - GameTooltip:SetOwner(s, "ANCHOR_NONE") - GameTooltip:SetPoint("TOPLEFT", s, "BOTTOMLEFT") - GameTooltip:ClearLines() - GameTooltip:AddLine(tpString) - GameTooltip:Show() - end) - - row:SetScript("OnLeave", function() - GameTooltip:Hide() - end) - - return row -end - -function ML:Show() - if not created then - ML:Create() - end - if frame:IsShown() then return end - frame:Show() -end - -function ML:Hide() - if not frame or not frame:IsShown() then return end - frame:Hide() -end - -function ML:Toggle() - if frame and frame:IsShown() then - ML:Hide() - else - ML:Show() - end -end - -function ML:Create() - if frame then return end - frame = CreateFrame("Frame", nil, UIParent, BackdropTemplateMixin and "BackdropTemplate") - frame:Hide() - frame:SetToplevel(true) - frame:EnableMouse(true) - frame:SetMovable(true) - frame:SetPoint("CENTER") - frame:SetWidth(FRAME_WIDTH) - frame:SetHeight(FRAME_HEIGHT) - - local bd = { - bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", - edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", - tile = true, - edgeSize = 16, - tileSize = 32, - insets = { - left = 2.5, - right = 2.5, - top = 2.5, - bottom = 2.5 - } - } - - frame:SetBackdrop(bd) - - ---@diagnostic disable-next-line: inject-field - frame.titleLabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal") - frame.titleLabel:SetWidth(250) - frame.titleLabel:SetHeight(16) - frame.titleLabel:SetPoint("TOP", frame, "TOP", 0, -5) - frame.titleLabel:SetText("KillTrack Mob Database (" .. KT.Version .. ")") - - frame:SetScript("OnMouseDown", function(s) s:StartMoving() end) - frame:SetScript("OnMouseUp", function(s) s:StopMovingOrSizing() end) - frame:SetScript("OnShow", function() ML:UpdateMobs(Sort, LastFilter) ML:UpdateEntries(LastOffset) end) - - ---@diagnostic disable-next-line: inject-field - frame.closeButton = CreateFrame("Button", nil, frame, "UIPanelCloseButton") - frame.closeButton:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -1, -1) - frame.closeButton:SetScript("OnClick", function() ML:Hide() end) - - ---@diagnostic disable-next-line: inject-field - frame.purgeButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") - frame.purgeButton:SetHeight(24) - frame.purgeButton:SetWidth(100) - frame.purgeButton:SetPoint("TOPLEFT", frame, "TOPLEFT", 10, -5) - frame.purgeButton:SetText("Purge Data") - frame.purgeButton:SetScript("OnClick", function() - KT:ShowPurge() - end) - - ---@diagnostic disable-next-line: inject-field - frame.resetButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") - frame.resetButton:SetHeight(24) - frame.resetButton:SetWidth(100) - frame.resetButton:SetPoint("TOPLEFT", frame.purgeButton, "BOTTOMLEFT", 0, -3) - frame.resetButton:SetText("Reset All") - frame.resetButton:SetScript("OnClick", function() - KT:ShowReset() - end) - - ---@diagnostic disable-next-line: inject-field - frame.helpLabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal") - frame.helpLabel:SetWidth(380) - frame.helpLabel:SetHeight(16) - frame.helpLabel:SetPoint("TOP", frame, "TOP", 0, -28) - frame.helpLabel:SetWordWrap(true) - frame.helpLabel:SetMaxLines(2) - frame.helpLabel:SetText( - "Click on an individual entry to delete it from the database. Use the search to filter database by name.") - - ---@diagnostic disable-next-line: inject-field - frame.searchBox = CreateFrame("EditBox", "KillTrackMobListSearchBox", frame, "SearchBoxTemplate") - frame.searchBox:SetWidth(200) - frame.searchBox:SetHeight(16) - frame.searchBox:SetPoint("TOPLEFT", frame.resetButton, "BOTTOMLEFT", 8, -3) - frame.searchBox:HookScript("OnTextChanged", function(s) - local text = s:GetText() - if (not _G[s:GetName() .. "ClearButton"]:IsShown()) then - text = nil - LastFilter = nil - end - if not text or text == "" then - ML:UpdateMobs(Sort) - else - ML:UpdateMobs(Sort, text) - end - ML:UpdateEntries(LastOffset) - end) - frame.searchBox:SetScript("OnEnterPressed", function(s) s:ClearFocus() end) - ---@diagnostic disable-next-line: inject-field - frame.searchBox.clearButton = _G[frame.searchBox:GetName() .. "ClearButton"] - local sBoxOldFunc = frame.searchBox.clearButton:GetScript("OnHide") - frame.searchBox.clearButton:SetScript("OnHide", function(s) - if sBoxOldFunc then sBoxOldFunc(s) end - if not frame:IsShown() then return end - ML:UpdateMobs(Sort, nil) - ML:UpdateEntries(LastOffset) - end) - - ---@diagnostic disable-next-line: inject-field - frame.searchTipLabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal") - frame.searchTipLabel:SetWidth(200) - frame.searchTipLabel:SetHeight(16) - frame.searchTipLabel:SetPoint("LEFT", frame.searchBox, "RIGHT", -22, 0) - frame.searchTipLabel:SetText("(Supports Lua patterns)") - - ---@diagnostic disable-next-line: inject-field - frame.idHeader = CreateHeader(frame) - frame.idHeader:SetPoint("TOPLEFT", frame, "TOPLEFT", HEADER_LEFT, HEADER_TOP) - frame.idHeader:SetWidth(ID_WIDTH) - frame.idHeader:SetText("NPC ID") - frame.idHeader:SetScript("OnClick", function() - local sort = KT.Sort.IdAsc - if Sort == sort then - sort = KT.Sort.IdDesc - end - ML:UpdateMobs(sort, LastFilter) - ML:UpdateEntries(LastOffset) - end) - - ---@diagnostic disable-next-line: inject-field - frame.nameHeader = CreateHeader(frame) - frame.nameHeader:SetPoint("TOPLEFT", frame.idHeader, "TOPRIGHT", -2, 0) - frame.nameHeader:SetWidth(NAME_WIDTH - SCROLL_WIDTH) - frame.nameHeader:SetText("Name") - frame.nameHeader:SetScript("OnClick", function() - local sort = KT.Sort.AlphaA - if Sort == sort then - sort = KT.Sort.AlphaD - end - ML:UpdateMobs(sort, LastFilter) - ML:UpdateEntries(LastOffset) - end) - - ---@diagnostic disable-next-line: inject-field - frame.charKillHeader = CreateHeader(frame) - frame.charKillHeader:SetPoint("TOPLEFT", frame.nameHeader, "TOPRIGHT", -2, 0) - frame.charKillHeader:SetWidth(CHAR_WIDTH) - frame.charKillHeader:SetText("Character Kills") - frame.charKillHeader:SetScript("OnClick", function() - local sort = KT.Sort.CharDesc - if Sort == sort then - sort = KT.Sort.CharAsc - end - ML:UpdateMobs(sort, LastFilter) - ML:UpdateEntries(LastOffset) - end) - - ---@diagnostic disable-next-line: inject-field - frame.globalKillHeader = CreateHeader(frame) - frame.globalKillHeader:SetPoint("TOPLEFT", frame.charKillHeader, "TOPRIGHT", -2, 0) - frame.globalKillHeader:SetWidth(GLOBAL_WIDTH + HEADER_LEFT) - frame.globalKillHeader:SetText("Global Kills") - frame.globalKillHeader:SetScript("OnClick", function() - local sort = KT.Sort.Desc - if Sort == sort then - sort = KT.Sort.Asc - end - ML:UpdateMobs(sort, LastFilter) - ML:UpdateEntries(LastOffset) - end) - - ---@diagnostic disable-next-line: inject-field - frame.rows = CreateFrame("Frame", nil, frame) - frame.rows:SetPoint("LEFT") - frame.rows:SetPoint("RIGHT", frame, "RIGHT", -SCROLL_WIDTH, 0) - frame.rows:SetPoint("TOP", frame.idHeader, "BOTTOM", 0, 0) - frame.rows:SetPoint("BOTTOM", frame, "BOTTOM", 0, 0) - frame.rows:SetPoint("TOPLEFT", frame.idHeader, "BOTTOMLEFT", 0, 30) - - local previous = frame.idHeader - for i = 1, ROW_COUNT do - local key = "row" .. i - frame.rows[key] = CreateRow(frame.rows, previous) - frame.rows[key].idField:SetText("") - frame.rows[key].nameField:SetText("") - frame.rows[key].charKillField:SetText("") - frame.rows[key].globalKillField:SetText("") - previous = frame.rows[key] - end - - ---@diagnostic disable-next-line: inject-field - frame.rows.scroller = CreateFrame( - "ScrollFrame", - "KillTrackMobListScrollFrame", - frame.rows, - "FauxScrollFrameTemplateLight") - frame.rows.scroller.name = frame.rows.scroller:GetName() - frame.rows.scroller:SetWidth(frame.rows:GetWidth()) - frame.rows.scroller:SetPoint("TOPRIGHT", frame.rows, "TOPRIGHT", -1, -2) - frame.rows.scroller:SetPoint("BOTTOMRIGHT", 0, 4) - frame.rows.scroller:SetScript( - "OnVerticalScroll", - function(s, val) - FauxScrollFrame_OnVerticalScroll( - s, val, ROW_HEIGHT, - function() - local offset = FauxScrollFrame_GetOffset(frame.rows.scroller) - ML:UpdateEntries(offset) - end - ) - end - ) - - self:UpdateMobs(Sort) - - ---@diagnostic disable-next-line: inject-field - frame.statusLabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal") - frame.statusLabel:SetWidth(420) - frame.statusLabel:SetHeight(16) - frame.statusLabel:SetPoint("BOTTOM", frame, "BOTTOM", 0, 8) - frame.statusLabel:SetText(STATUS_TEXT:format(1, ROW_COUNT, #Mobs)) - - self:UpdateEntries(LastOffset) - - created = true -end - ----@param sort KillTrackMobSortMode? ----@param filter string? -function ML:UpdateMobs(sort, filter) - sort = (sort or Sort) or KT.Sort.Desc - Sort = sort - LastFilter = filter - Mobs = KT:GetSortedMobTable(Sort, filter and filter:lower() or nil) - FauxScrollFrame_Update(frame.rows.scroller, #Mobs, ROW_COUNT, ROW_HEIGHT) -end - ----@param offset integer? -function ML:UpdateEntries(offset) - if (#Mobs <= 0) then - for i = 1, ROW_COUNT do - local row = frame.rows["row" .. i] - row.idField:SetText("") - if i == 1 then - row.nameField:SetText("No entries in database or none matched search!") - else - row.nameField:SetText("") - end - row.charKillField:SetText("") - row.globalKillField:SetText("") - row:Disable() - end - - frame.statusLabel:SetText(STATUS_TEXT:format(0, 0, 0)) - - return - elseif #Mobs < ROW_COUNT then - for i = 1, ROW_COUNT do - local row = frame.rows["row" .. i] - row.idField:SetText("") - row.nameField:SetText("") - row.charKillField:SetText("") - row.globalKillField:SetText("") - row:Disable() - end - end - offset = (tonumber(offset) or LastOffset) or 0 - LastOffset = offset - local limit = ROW_COUNT - if limit > #Mobs then - limit = #Mobs - end - for i = 1, limit do - local row = frame.rows["row" .. i] - local mob = Mobs[i + offset] - row.idField:SetText(mob.Id) - row.nameField:SetText(mob.Name) - row.charKillField:SetText(mob.cKills) - row.globalKillField:SetText(mob.gKills) - row:Enable() - end - - local mobCount = KTT:TableLength(KT.Global.MOBS) - local hidden = mobCount - #Mobs - frame.statusLabel:SetText(STATUS_TEXT:format(1 + offset, math.min(#Mobs, offset + ROW_COUNT), #Mobs, hidden)) - - if offset == 0 then - _G[frame.rows.scroller.name .. "ScrollBarScrollUpButton"]:Disable() - else - _G[frame.rows.scroller.name .. "ScrollBarScrollUpButton"]:Enable() - end - - if offset + ROW_COUNT == #Mobs then - _G[frame.rows.scroller.name .. "ScrollBarScrollDownButton"]:Disable() - else - _G[frame.rows.scroller.name .. "ScrollBarScrollDownButton"]:Enable() - end -end +--[[ + * Copyright (c) 2011-2020 by Adam Hellberg. + * + * This file is part of KillTrack. + * + * KillTrack is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KillTrack is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KillTrack. If not, see . +--]] + +-- Beware of some possibly messy code in this file + +---@class KillTrack +local KT = select(2, ...) + +---@class KillTrackMobList +local ML = {} + +KT.MobList = ML +local KTT = KT.Tools + +local Sort = KT.Sort.Desc +---@type { Id: integer, Name: string, gKills: integer, cKills: integer }[] +local Mobs = nil +local LastFilter = nil +local LastOffset = 0 + +-- Frame Constants +local FRAME_WIDTH = 600 +local FRAME_HEIGHT = 534 +local HEADER_HEIGHT = 24 +local HEADER_LEFT = 3 +local HEADER_TOP = -80 +local ROW_HEIGHT = 15 +local ROW_COUNT = 27 +local ROW_TEXT_PADDING = 5 +local ID_WIDTH = 100 +local NAME_WIDTH = 300 +local CHAR_WIDTH = 100 +local GLOBAL_WIDTH = 100 +local SCROLL_WIDTH = 27 -- Scrollbar width +local STATUS_TEXT = "Showing entries %d through %d out of %d total (%d hidden)" + +---@type table|BackdropTemplate|Frame +local frame = nil +local created = false + +-- Frame helper functions + +---@param parent table|Frame +---@return Button +local function CreateHeader(parent) + local h = CreateFrame("Button", nil, parent) + h:SetHeight(HEADER_HEIGHT) + h:SetNormalFontObject("GameFontHighlightSmall") + + local bgl = h:CreateTexture(nil, "BACKGROUND") + bgl:SetTexture("Interface\\FriendsFrame\\WhoFrame-ColumnTabs") + bgl:SetWidth(5) + bgl:SetHeight(HEADER_HEIGHT) + bgl:SetPoint("TOPLEFT") + bgl:SetTexCoord(0, 0.07815, 0, 0.75) + + local bgr = h:CreateTexture(nil, "BACKGROUND") + bgr:SetTexture("Interface\\FriendsFrame\\WhoFrame-ColumnTabs") + bgr:SetWidth(5) + bgr:SetHeight(HEADER_HEIGHT) + bgr:SetPoint("TOPRIGHT") + bgr:SetTexCoord(0.90625, 0.96875, 0, 0.75) + + local bgm = h:CreateTexture(nil, "BACKGROUND") + bgm:SetTexture("Interface\\FriendsFrame\\WhoFrame-ColumnTabs") + bgm:SetHeight(HEADER_HEIGHT) + bgm:SetPoint("LEFT", bgl, "RIGHT") + bgm:SetPoint("RIGHT", bgr, "LEFT") + bgm:SetTexCoord(0.07815, 0.90625, 0, 0.75) + + local hl = h:CreateTexture() + h:SetHighlightTexture("Interface\\PaperDollInfoFrame\\UI-Character-Tab-Highlight", "ADD") + hl:SetPoint("TOPLEFT", bgl, "TOPLEFT", -2, 5) + hl:SetPoint("BOTTOMRIGHT", bgr, "BOTTOMRIGHT", 2, -7) + + return h +end + +---@param container table|Frame +---@param previous table|Frame +---@return Button +local function CreateRow(container, previous) + local row = CreateFrame("Button", nil, container) + row:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight", "ADD") + row:SetHeight(ROW_HEIGHT) + row:SetPoint("LEFT") + row:SetPoint("RIGHT") + row:SetPoint("TOPLEFT", previous, "BOTTOMLEFT", 0, 0) + + ---@diagnostic disable-next-line: inject-field + row.idField = row:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") + row.idField:SetHeight(ROW_HEIGHT) + row.idField:SetWidth(ID_WIDTH - ROW_TEXT_PADDING * 2) + row.idField:SetPoint("LEFT", row, "LEFT", ROW_TEXT_PADDING, 0) + row.idField:SetJustifyH("RIGHT") + + ---@diagnostic disable-next-line: inject-field + row.nameField = row:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") + row.nameField:SetHeight(ROW_HEIGHT) + row.nameField:SetWidth(NAME_WIDTH - SCROLL_WIDTH - ROW_TEXT_PADDING * 3) + row.nameField:SetPoint("LEFT", row.idField, "RIGHT", 2 * ROW_TEXT_PADDING, 0) + row.nameField:SetJustifyH("LEFT") + + ---@diagnostic disable-next-line: inject-field + row.charKillField = row:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") + row.charKillField:SetHeight(ROW_HEIGHT) + row.charKillField:SetWidth(CHAR_WIDTH - ROW_TEXT_PADDING * 2) + row.charKillField:SetPoint("LEFT", row.nameField, "RIGHT", 2 * ROW_TEXT_PADDING, 0) + row.charKillField:SetJustifyH("RIGHT") + + ---@diagnostic disable-next-line: inject-field + row.globalKillField = row:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") + row.globalKillField:SetHeight(ROW_HEIGHT) + row.globalKillField:SetWidth(GLOBAL_WIDTH - ROW_TEXT_PADDING * 2) + row.globalKillField:SetPoint("LEFT", row.charKillField, "RIGHT", 2 * ROW_TEXT_PADDING, 0) + row.globalKillField:SetJustifyH("RIGHT") + + row:SetScript("OnClick", function(s) + local id = tonumber(s.idField:GetText()) + if not id then return end + local name = s.nameField:GetText() + KT:ShowDelete(id, name) + end) + + row:SetScript("OnEnter", function(s) + local id = tonumber(s.idField:GetText()) + if not id then return end + local globalData = KT.Global.MOBS[id] + if not globalData then return end + local killTimestamp = globalData.LastKillAt + if not killTimestamp then return end + local lastKillAt = KTT:FormatDateTime(killTimestamp) + local tpString = ("Last killed at %s"):format(lastKillAt) + GameTooltip:SetOwner(s, "ANCHOR_NONE") + GameTooltip:SetPoint("TOPLEFT", s, "BOTTOMLEFT") + GameTooltip:ClearLines() + GameTooltip:AddLine(tpString) + GameTooltip:Show() + end) + + row:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + + return row +end + +function ML:Show() + if not created then + ML:Create() + end + if frame:IsShown() then return end + frame:Show() +end + +function ML:Hide() + if not frame or not frame:IsShown() then return end + frame:Hide() +end + +function ML:Toggle() + if frame and frame:IsShown() then + ML:Hide() + else + ML:Show() + end +end + +function ML:Create() + if frame then return end + frame = CreateFrame("Frame", nil, UIParent, BackdropTemplateMixin and "BackdropTemplate") + frame:Hide() + frame:SetToplevel(true) + frame:EnableMouse(true) + frame:SetMovable(true) + frame:SetPoint("CENTER") + frame:SetWidth(FRAME_WIDTH) + frame:SetHeight(FRAME_HEIGHT) + + local bd = { + bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, + edgeSize = 16, + tileSize = 32, + insets = { + left = 2.5, + right = 2.5, + top = 2.5, + bottom = 2.5 + } + } + + frame:SetBackdrop(bd) + + ---@diagnostic disable-next-line: inject-field + frame.titleLabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal") + frame.titleLabel:SetWidth(250) + frame.titleLabel:SetHeight(16) + frame.titleLabel:SetPoint("TOP", frame, "TOP", 0, -5) + frame.titleLabel:SetText("KillTrack Mob Database (" .. KT.Version .. ")") + + frame:SetScript("OnMouseDown", function(s) s:StartMoving() end) + frame:SetScript("OnMouseUp", function(s) s:StopMovingOrSizing() end) + frame:SetScript("OnShow", function() ML:UpdateMobs(Sort, LastFilter) ML:UpdateEntries(LastOffset) end) + + ---@diagnostic disable-next-line: inject-field + frame.closeButton = CreateFrame("Button", nil, frame, "UIPanelCloseButton") + frame.closeButton:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -1, -1) + frame.closeButton:SetScript("OnClick", function() ML:Hide() end) + + ---@diagnostic disable-next-line: inject-field + frame.purgeButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") + frame.purgeButton:SetHeight(24) + frame.purgeButton:SetWidth(100) + frame.purgeButton:SetPoint("TOPLEFT", frame, "TOPLEFT", 10, -5) + frame.purgeButton:SetText("Purge Data") + frame.purgeButton:SetScript("OnClick", function() + KT:ShowPurge() + end) + + ---@diagnostic disable-next-line: inject-field + frame.resetButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") + frame.resetButton:SetHeight(24) + frame.resetButton:SetWidth(100) + frame.resetButton:SetPoint("TOPLEFT", frame.purgeButton, "BOTTOMLEFT", 0, -3) + frame.resetButton:SetText("Reset All") + frame.resetButton:SetScript("OnClick", function() + KT:ShowReset() + end) + + ---@diagnostic disable-next-line: inject-field + frame.helpLabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal") + frame.helpLabel:SetWidth(380) + frame.helpLabel:SetHeight(16) + frame.helpLabel:SetPoint("TOP", frame, "TOP", 0, -28) + frame.helpLabel:SetWordWrap(true) + frame.helpLabel:SetMaxLines(2) + frame.helpLabel:SetText( + "Click on an individual entry to delete it from the database. Use the search to filter database by name.") + + ---@diagnostic disable-next-line: inject-field + frame.searchBox = CreateFrame("EditBox", "KillTrackMobListSearchBox", frame, "SearchBoxTemplate") + frame.searchBox:SetWidth(200) + frame.searchBox:SetHeight(16) + frame.searchBox:SetPoint("TOPLEFT", frame.resetButton, "BOTTOMLEFT", 8, -3) + frame.searchBox:HookScript("OnTextChanged", function(s) + local text = s:GetText() + if (not _G[s:GetName() .. "ClearButton"]:IsShown()) then + text = nil + LastFilter = nil + end + if not text or text == "" then + ML:UpdateMobs(Sort) + else + ML:UpdateMobs(Sort, text) + end + ML:UpdateEntries(LastOffset) + end) + frame.searchBox:SetScript("OnEnterPressed", function(s) s:ClearFocus() end) + ---@diagnostic disable-next-line: inject-field + frame.searchBox.clearButton = _G[frame.searchBox:GetName() .. "ClearButton"] + local sBoxOldFunc = frame.searchBox.clearButton:GetScript("OnHide") + frame.searchBox.clearButton:SetScript("OnHide", function(s) + if sBoxOldFunc then sBoxOldFunc(s) end + if not frame:IsShown() then return end + ML:UpdateMobs(Sort, nil) + ML:UpdateEntries(LastOffset) + end) + + ---@diagnostic disable-next-line: inject-field + frame.searchTipLabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal") + frame.searchTipLabel:SetWidth(200) + frame.searchTipLabel:SetHeight(16) + frame.searchTipLabel:SetPoint("LEFT", frame.searchBox, "RIGHT", -22, 0) + frame.searchTipLabel:SetText("(Supports Lua patterns)") + + ---@diagnostic disable-next-line: inject-field + frame.idHeader = CreateHeader(frame) + frame.idHeader:SetPoint("TOPLEFT", frame, "TOPLEFT", HEADER_LEFT, HEADER_TOP) + frame.idHeader:SetWidth(ID_WIDTH) + frame.idHeader:SetText("NPC ID") + frame.idHeader:SetScript("OnClick", function() + local sort = KT.Sort.IdAsc + if Sort == sort then + sort = KT.Sort.IdDesc + end + ML:UpdateMobs(sort, LastFilter) + ML:UpdateEntries(LastOffset) + end) + + ---@diagnostic disable-next-line: inject-field + frame.nameHeader = CreateHeader(frame) + frame.nameHeader:SetPoint("TOPLEFT", frame.idHeader, "TOPRIGHT", -2, 0) + frame.nameHeader:SetWidth(NAME_WIDTH - SCROLL_WIDTH) + frame.nameHeader:SetText("Name") + frame.nameHeader:SetScript("OnClick", function() + local sort = KT.Sort.AlphaA + if Sort == sort then + sort = KT.Sort.AlphaD + end + ML:UpdateMobs(sort, LastFilter) + ML:UpdateEntries(LastOffset) + end) + + ---@diagnostic disable-next-line: inject-field + frame.charKillHeader = CreateHeader(frame) + frame.charKillHeader:SetPoint("TOPLEFT", frame.nameHeader, "TOPRIGHT", -2, 0) + frame.charKillHeader:SetWidth(CHAR_WIDTH) + frame.charKillHeader:SetText("Character Kills") + frame.charKillHeader:SetScript("OnClick", function() + local sort = KT.Sort.CharDesc + if Sort == sort then + sort = KT.Sort.CharAsc + end + ML:UpdateMobs(sort, LastFilter) + ML:UpdateEntries(LastOffset) + end) + + ---@diagnostic disable-next-line: inject-field + frame.globalKillHeader = CreateHeader(frame) + frame.globalKillHeader:SetPoint("TOPLEFT", frame.charKillHeader, "TOPRIGHT", -2, 0) + frame.globalKillHeader:SetWidth(GLOBAL_WIDTH + HEADER_LEFT) + frame.globalKillHeader:SetText("Global Kills") + frame.globalKillHeader:SetScript("OnClick", function() + local sort = KT.Sort.Desc + if Sort == sort then + sort = KT.Sort.Asc + end + ML:UpdateMobs(sort, LastFilter) + ML:UpdateEntries(LastOffset) + end) + + ---@diagnostic disable-next-line: inject-field + frame.rows = CreateFrame("Frame", nil, frame) + frame.rows:SetPoint("LEFT") + frame.rows:SetPoint("RIGHT", frame, "RIGHT", -SCROLL_WIDTH, 0) + frame.rows:SetPoint("TOP", frame.idHeader, "BOTTOM", 0, 0) + frame.rows:SetPoint("BOTTOM", frame, "BOTTOM", 0, 0) + frame.rows:SetPoint("TOPLEFT", frame.idHeader, "BOTTOMLEFT", 0, 30) + + local previous = frame.idHeader + for i = 1, ROW_COUNT do + local key = "row" .. i + frame.rows[key] = CreateRow(frame.rows, previous) + frame.rows[key].idField:SetText("") + frame.rows[key].nameField:SetText("") + frame.rows[key].charKillField:SetText("") + frame.rows[key].globalKillField:SetText("") + previous = frame.rows[key] + end + + ---@diagnostic disable-next-line: inject-field + frame.rows.scroller = CreateFrame( + "ScrollFrame", + "KillTrackMobListScrollFrame", + frame.rows, + "FauxScrollFrameTemplateLight") + frame.rows.scroller.name = frame.rows.scroller:GetName() + frame.rows.scroller:SetWidth(frame.rows:GetWidth()) + frame.rows.scroller:SetPoint("TOPRIGHT", frame.rows, "TOPRIGHT", -1, -2) + frame.rows.scroller:SetPoint("BOTTOMRIGHT", 0, 4) + frame.rows.scroller:SetScript( + "OnVerticalScroll", + function(s, val) + FauxScrollFrame_OnVerticalScroll( + s, val, ROW_HEIGHT, + function() + local offset = FauxScrollFrame_GetOffset(frame.rows.scroller) + ML:UpdateEntries(offset) + end + ) + end + ) + + self:UpdateMobs(Sort) + + ---@diagnostic disable-next-line: inject-field + frame.statusLabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal") + frame.statusLabel:SetWidth(420) + frame.statusLabel:SetHeight(16) + frame.statusLabel:SetPoint("BOTTOM", frame, "BOTTOM", 0, 8) + frame.statusLabel:SetText(STATUS_TEXT:format(1, ROW_COUNT, #Mobs)) + + self:UpdateEntries(LastOffset) + + created = true +end + +---@param sort KillTrackMobSortMode? +---@param filter string? +function ML:UpdateMobs(sort, filter) + sort = (sort or Sort) or KT.Sort.Desc + Sort = sort + LastFilter = filter + Mobs = KT:GetSortedMobTable(Sort, filter and filter:lower() or nil) + FauxScrollFrame_Update(frame.rows.scroller, #Mobs, ROW_COUNT, ROW_HEIGHT) +end + +---@param offset integer? +function ML:UpdateEntries(offset) + if (#Mobs <= 0) then + for i = 1, ROW_COUNT do + local row = frame.rows["row" .. i] + row.idField:SetText("") + if i == 1 then + row.nameField:SetText("No entries in database or none matched search!") + else + row.nameField:SetText("") + end + row.charKillField:SetText("") + row.globalKillField:SetText("") + row:Disable() + end + + frame.statusLabel:SetText(STATUS_TEXT:format(0, 0, 0)) + + return + elseif #Mobs < ROW_COUNT then + for i = 1, ROW_COUNT do + local row = frame.rows["row" .. i] + row.idField:SetText("") + row.nameField:SetText("") + row.charKillField:SetText("") + row.globalKillField:SetText("") + row:Disable() + end + end + offset = (tonumber(offset) or LastOffset) or 0 + LastOffset = offset + local limit = ROW_COUNT + if limit > #Mobs then + limit = #Mobs + end + for i = 1, limit do + local row = frame.rows["row" .. i] + local mob = Mobs[i + offset] + row.idField:SetText(mob.Id) + row.nameField:SetText(mob.Name) + row.charKillField:SetText(mob.cKills) + row.globalKillField:SetText(mob.gKills) + row:Enable() + end + + local mobCount = KTT:TableLength(KT.Global.MOBS) + local hidden = mobCount - #Mobs + frame.statusLabel:SetText(STATUS_TEXT:format(1 + offset, math.min(#Mobs, offset + ROW_COUNT), #Mobs, hidden)) + + if offset == 0 then + _G[frame.rows.scroller.name .. "ScrollBarScrollUpButton"]:Disable() + else + _G[frame.rows.scroller.name .. "ScrollBarScrollUpButton"]:Enable() + end + + if offset + ROW_COUNT == #Mobs then + _G[frame.rows.scroller.name .. "ScrollBarScrollDownButton"]:Disable() + else + _G[frame.rows.scroller.name .. "ScrollBarScrollDownButton"]:Enable() + end +end diff --git a/Options.lua b/Options.lua index ee67e18..aca9bf9 100644 --- a/Options.lua +++ b/Options.lua @@ -1,276 +1,276 @@ ---[[ - * Copyright (c) 2011-2020 by Adam Hellberg. - * - * This file is part of KillTrack. - * - * KillTrack is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KillTrack is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KillTrack. If not, see . ---]] - ----@class KillTrack -local KT = select(2, ...) - -local KTT = KT.Tools - ----@class KillTrackOptions -local Opt = { - Panel = CreateFrame("Frame") -} - -KT.Options = Opt - -local panel = Opt.Panel - ----@diagnostic disable-next-line: inject-field -panel.name = "KillTrack" -panel:Hide() - -local category = Settings.RegisterCanvasLayoutCategory(panel, panel.name) -Opt.Category = category - --- Dirty hack to give a name to option checkboxes -local checkCounter = 0 - ----@param label string ----@param description string ----@param onclick function ----@return table|CheckButton -local function checkbox(label, description, onclick) - local check = CreateFrame( - "CheckButton", - "KillTrackOptCheck" .. checkCounter, - panel, - "InterfaceOptionsCheckButtonTemplate") - check:SetScript("OnClick", function(self) - local checked = self:GetChecked() - PlaySound(checked and SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON or SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_OFF) - onclick(self, checked and true or false) - end) - ---@diagnostic disable-next-line: inject-field - check.label = _G[check:GetName() .. "Text"] - check.label:SetText(label) - ---@diagnostic disable-next-line: inject-field - check.tooltipText = label - ---@diagnostic disable-next-line: inject-field - check.tooltipRequirement = description - checkCounter = checkCounter + 1 - return check -end - ----@param text string ----@param tooltip string ----@param onclick function ----@return table|Button -local function button(text, tooltip, onclick) - local btn = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate") - btn:SetText(text) - ---@diagnostic disable-next-line: inject-field - btn.tooltipText = tooltip - btn:SetScript("OnClick", function(self) onclick(self) end) - btn:SetHeight(24) - return btn -end - -local function HideBlizzOptions() - HideUIPanel(InterfaceOptionsFrame) - HideUIPanel(GameMenuFrame) -end - -function Opt:Open() - Settings.OpenToCategory(self.Category.ID) -end - ----@param self Frame -function Opt.Show(self) - local title = self:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge") - title:SetPoint("TOPLEFT", 16, -16) - title:SetText("KillTrack") - - local printKills = checkbox("Print kill updates to chat", - "With this enabled, every kill you make is going to be announced locally in the chatbox", - function(_, checked) KT.Global.PRINTKILLS = checked end) - printKills:SetPoint("TOPLEFT", title, "BOTTOMLEFT", -2, -16) - - local tooltipControl = checkbox("Show mob data in tooltip", - "With this enabled, KillTrack will print data about mobs in the tooltip", - function(_, checked) KT.Global.TOOLTIP = checked end) - tooltipControl:SetPoint("LEFT", printKills, "RIGHT", 180, 0) - - local printNew = checkbox("Print new mob entries to chat", - "With this enabled, new mobs added to the database will be announced locally in the chat", - function(_, checked) KT.Global.PRINTNEW = checked end) - printNew:SetPoint("TOPLEFT", printKills, "BOTTOMLEFT", 0, -8) - - local countGroup = checkbox("Count group kills", - "With this disabled, only killing blows made by yourself will count", - function(_, checked) KT.Global.COUNT_GROUP = checked end) - countGroup:SetPoint("TOPLEFT", printNew, "BOTTOMLEFT", 0, -8) - - local thresholdDesc = self:CreateFontString(nil, "ARTWORK", "GameFontNormal") - thresholdDesc:SetPoint("TOPLEFT", countGroup, "BOTTOMLEFT", 0, -8) - thresholdDesc:SetTextColor(1, 1, 1) - thresholdDesc:SetText("Threshold for displaying kill achievements (press enter to apply)") - - local threshold = CreateFrame("EditBox", "KillTrackOptThreshold", panel, "InputBoxTemplate") - threshold:SetHeight(22) - threshold:SetWidth(150) - threshold:SetPoint("LEFT", thresholdDesc, "RIGHT", 8, 0) - threshold:SetAutoFocus(false) - threshold:EnableMouse(true) - threshold:SetScript("OnEditFocusGained", function(box) - box:SetTextColor(0, 1, 0) - box:HighlightText() - end) - local function setThreshold(box, enter) - box:SetTextColor(1, 1, 1) - local value = tonumber(box:GetNumber()) - if value and value > 0 then - KT.Global.ACHIEV_THRESHOLD = value - if not enter then - KT:Msg("Updated threshold value!") - end - box:ClearFocus() - box:SetText(KT.Global.ACHIEV_THRESHOLD) - else - box:SetText(KT.Global.ACHIEV_THRESHOLD) - box:HighlightText() - end - end - threshold:SetScript("OnEditFocusLost", function(box) setThreshold(box) end) - threshold:SetScript("OnEnterPressed", function(box) setThreshold(box, true) end) - - local showTarget = button("Target", "Show information about the currently selected target", - function() - if not UnitExists("target") or UnitIsPlayer("target") then return end - local id = KTT.GUIDToID(UnitGUID("target")) - KT:PrintKills(id) - end) - showTarget:SetWidth(150) - showTarget:SetPoint("TOPLEFT", thresholdDesc, "BOTTOMLEFT", 0, -8) - - local list = button("List", "Open the mob database", - function() - HideBlizzOptions() - KT.MobList:Show() - end) - list:SetWidth(150) - list:SetPoint("TOPLEFT", showTarget, "TOPRIGHT", 8, 0) - - local purge = button("Purge", "Purge mob entries with a kill count below a specified number", - function() KT:ShowPurge() end) - purge:SetWidth(150) - purge:SetPoint("TOPLEFT", showTarget, "BOTTOMLEFT", 0, -8) - - local reset = button("Reset", "Clear the database of ALL mob entries", - function() KT:ShowReset() end) - reset:SetWidth(150) - reset:SetPoint("TOPLEFT", purge, "TOPRIGHT", 8, 0) - - local minimap = checkbox("Show minimap icon", "Adds the KillTrack broker to your minimap", - function(_, checked) KT.Broker:SetMinimap(checked) end) - minimap:SetPoint("TOPLEFT", purge, "BOTTOMLEFT", 0, -8) - - local disableDungeons = checkbox("Disable in dungeons (save CPU)", - "When this is checked, mob kills in dungeons won't be counted.", - function(_, checked) KT.Global.DISABLE_DUNGEONS = checked end) - disableDungeons:SetPoint("TOPLEFT", minimap, "BOTTOMLEFT", 0, -8) - - local disableRaids = checkbox("Disable in raids (save CPU)", - "When this is checked, mob kills in raids won't be counted.", - function(_, checked) KT.Global.DISABLE_RAIDS = checked end) - disableRaids:SetPoint("TOPLEFT", disableDungeons, "BOTTOMLEFT", 0, -8) - - local datetimeFormatDesc = self:CreateFontString(nil, "ARTWORK", "GameFontNormal") - datetimeFormatDesc:SetPoint("TOPLEFT", disableRaids, "BOTTOMLEFT", 0, -8) - datetimeFormatDesc:SetTextColor(1, 1, 1) - datetimeFormatDesc:SetText("Datetime format template (press enter to apply)") - - local datetimeFormat = CreateFrame("EditBox", "KillTrackOptDateTimeFormat", panel, "InputBoxTemplate") - datetimeFormat:SetHeight(22) - datetimeFormat:SetWidth(200) - datetimeFormat:SetPoint("LEFT", datetimeFormatDesc, "RIGHT", 8, 0) - datetimeFormat:SetAutoFocus(false) - datetimeFormat:EnableMouse(true) - local datetimeFormatPreview = self:CreateFontString(nil, "ARTWORK", "GameFontNormal") - datetimeFormatPreview:SetPoint("TOPLEFT", datetimeFormat, "BOTTOMLEFT", 0, -2) - datetimeFormatPreview:SetTextColor(1, 1, 1) - datetimeFormatPreview:SetText("Preview:") - local datetimeFormatPreviewValue = self:CreateFontString(nil, "ARTWORK", "GameFontNormal") - datetimeFormatPreviewValue:SetPoint("LEFT", datetimeFormatPreview, "RIGHT", 8, 0) - datetimeFormatPreviewValue:SetTextColor(1, 1, 1) - datetimeFormatPreviewValue:SetText(KTT:FormatDateTime() --[[@as string]]) - - datetimeFormat:SetScript("OnEditFocusGained", function(box) - box:SetTextColor(0, 1, 0) - box:HighlightText() - end) - local function setDateTimeFormat(box, enter) - box:SetTextColor(1, 1, 1) - local value = box:GetText() - if type(value) ~= "string" then - box:SetText(KT.Global.DATETIME_FORMAT) - box:HighlightText() - return - end - local valid, errMsg = pcall(KTT.FormatDateTime, KTT, nil, value) - if not valid then - KT:Msg("Invalid format string: " .. (errMsg or "unknown error")) - box:HighlightText() - return - end - KT.Global.DATETIME_FORMAT = value - if not enter then - KT:Msg("Updated datetime format!") - end - box:ClearFocus() - box:SetText(KT.Global.DATETIME_FORMAT) - end - datetimeFormat:SetScript("OnEditFocusLost", function(box) setDateTimeFormat(box) end) - datetimeFormat:SetScript("OnEnterPressed", function(box) setDateTimeFormat(box, true) end) - datetimeFormat:SetScript("OnTextChanged", function(box) - local value = box:GetText() - if type(value) ~= "string" then return end - local valid, result = pcall(KTT.FormatDateTime, KTT, nil, value) - if valid then - datetimeFormatPreviewValue:SetText(result --[[@as string]]) - else - datetimeFormatPreviewValue:SetText("invalid format") - end - end) - local datetimeFormatReset = button("Reset", "Reset the datetime format to the default", function() - KT.Global.DATETIME_FORMAT = KT.Defaults.DateTimeFormat - datetimeFormat:SetText(KT.Global.DATETIME_FORMAT) - end) - datetimeFormatReset:SetWidth(80) - datetimeFormatReset:SetPoint("LEFT", datetimeFormat, "RIGHT", 5, 0) - - local function init() - printKills:SetChecked(KT.Global.PRINTKILLS) - tooltipControl:SetChecked(KT.Global.TOOLTIP) - printNew:SetChecked(KT.Global.PRINTNEW) - countGroup:SetChecked(KT.Global.COUNT_GROUP) - threshold:SetText(tostring(KT.Global.ACHIEV_THRESHOLD)) - minimap:SetChecked(not KT.Global.BROKER.MINIMAP.hide) - disableDungeons:SetChecked(KT.Global.DISABLE_DUNGEONS) - disableRaids:SetChecked(KT.Global.DISABLE_RAIDS) - datetimeFormat:SetText(KT.Global.DATETIME_FORMAT) - end - - init() - - self:SetScript("OnShow", init) -end - -panel:SetScript("OnShow", function(self) Opt.Show(self) end) - -Settings.RegisterAddOnCategory(category) +--[[ + * Copyright (c) 2011-2020 by Adam Hellberg. + * + * This file is part of KillTrack. + * + * KillTrack is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KillTrack is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KillTrack. If not, see . +--]] + +---@class KillTrack +local KT = select(2, ...) + +local KTT = KT.Tools + +---@class KillTrackOptions +local Opt = { + Panel = CreateFrame("Frame") +} + +KT.Options = Opt + +local panel = Opt.Panel + +---@diagnostic disable-next-line: inject-field +panel.name = "KillTrack" +panel:Hide() + +local category = Settings.RegisterCanvasLayoutCategory(panel, panel.name) +Opt.Category = category + +-- Dirty hack to give a name to option checkboxes +local checkCounter = 0 + +---@param label string +---@param description string +---@param onclick function +---@return table|CheckButton +local function checkbox(label, description, onclick) + local check = CreateFrame( + "CheckButton", + "KillTrackOptCheck" .. checkCounter, + panel, + "InterfaceOptionsCheckButtonTemplate") + check:SetScript("OnClick", function(self) + local checked = self:GetChecked() + PlaySound(checked and SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON or SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_OFF) + onclick(self, checked and true or false) + end) + ---@diagnostic disable-next-line: inject-field + check.label = _G[check:GetName() .. "Text"] + check.label:SetText(label) + ---@diagnostic disable-next-line: inject-field + check.tooltipText = label + ---@diagnostic disable-next-line: inject-field + check.tooltipRequirement = description + checkCounter = checkCounter + 1 + return check +end + +---@param text string +---@param tooltip string +---@param onclick function +---@return table|Button +local function button(text, tooltip, onclick) + local btn = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate") + btn:SetText(text) + ---@diagnostic disable-next-line: inject-field + btn.tooltipText = tooltip + btn:SetScript("OnClick", function(self) onclick(self) end) + btn:SetHeight(24) + return btn +end + +local function HideBlizzOptions() + HideUIPanel(InterfaceOptionsFrame) + HideUIPanel(GameMenuFrame) +end + +function Opt:Open() + Settings.OpenToCategory(self.Category.ID) +end + +---@param self Frame +function Opt.Show(self) + local title = self:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge") + title:SetPoint("TOPLEFT", 16, -16) + title:SetText("KillTrack") + + local printKills = checkbox("Print kill updates to chat", + "With this enabled, every kill you make is going to be announced locally in the chatbox", + function(_, checked) KT.Global.PRINTKILLS = checked end) + printKills:SetPoint("TOPLEFT", title, "BOTTOMLEFT", -2, -16) + + local tooltipControl = checkbox("Show mob data in tooltip", + "With this enabled, KillTrack will print data about mobs in the tooltip", + function(_, checked) KT.Global.TOOLTIP = checked end) + tooltipControl:SetPoint("LEFT", printKills, "RIGHT", 180, 0) + + local printNew = checkbox("Print new mob entries to chat", + "With this enabled, new mobs added to the database will be announced locally in the chat", + function(_, checked) KT.Global.PRINTNEW = checked end) + printNew:SetPoint("TOPLEFT", printKills, "BOTTOMLEFT", 0, -8) + + local countGroup = checkbox("Count group kills", + "With this disabled, only killing blows made by yourself will count", + function(_, checked) KT.Global.COUNT_GROUP = checked end) + countGroup:SetPoint("TOPLEFT", printNew, "BOTTOMLEFT", 0, -8) + + local thresholdDesc = self:CreateFontString(nil, "ARTWORK", "GameFontNormal") + thresholdDesc:SetPoint("TOPLEFT", countGroup, "BOTTOMLEFT", 0, -8) + thresholdDesc:SetTextColor(1, 1, 1) + thresholdDesc:SetText("Threshold for displaying kill achievements (press enter to apply)") + + local threshold = CreateFrame("EditBox", "KillTrackOptThreshold", panel, "InputBoxTemplate") + threshold:SetHeight(22) + threshold:SetWidth(150) + threshold:SetPoint("LEFT", thresholdDesc, "RIGHT", 8, 0) + threshold:SetAutoFocus(false) + threshold:EnableMouse(true) + threshold:SetScript("OnEditFocusGained", function(box) + box:SetTextColor(0, 1, 0) + box:HighlightText() + end) + local function setThreshold(box, enter) + box:SetTextColor(1, 1, 1) + local value = tonumber(box:GetNumber()) + if value and value > 0 then + KT.Global.ACHIEV_THRESHOLD = value + if not enter then + KT:Msg("Updated threshold value!") + end + box:ClearFocus() + box:SetText(KT.Global.ACHIEV_THRESHOLD) + else + box:SetText(KT.Global.ACHIEV_THRESHOLD) + box:HighlightText() + end + end + threshold:SetScript("OnEditFocusLost", function(box) setThreshold(box) end) + threshold:SetScript("OnEnterPressed", function(box) setThreshold(box, true) end) + + local showTarget = button("Target", "Show information about the currently selected target", + function() + if not UnitExists("target") or UnitIsPlayer("target") then return end + local id = KTT.GUIDToID(UnitGUID("target")) + KT:PrintKills(id) + end) + showTarget:SetWidth(150) + showTarget:SetPoint("TOPLEFT", thresholdDesc, "BOTTOMLEFT", 0, -8) + + local list = button("List", "Open the mob database", + function() + HideBlizzOptions() + KT.MobList:Show() + end) + list:SetWidth(150) + list:SetPoint("TOPLEFT", showTarget, "TOPRIGHT", 8, 0) + + local purge = button("Purge", "Purge mob entries with a kill count below a specified number", + function() KT:ShowPurge() end) + purge:SetWidth(150) + purge:SetPoint("TOPLEFT", showTarget, "BOTTOMLEFT", 0, -8) + + local reset = button("Reset", "Clear the database of ALL mob entries", + function() KT:ShowReset() end) + reset:SetWidth(150) + reset:SetPoint("TOPLEFT", purge, "TOPRIGHT", 8, 0) + + local minimap = checkbox("Show minimap icon", "Adds the KillTrack broker to your minimap", + function(_, checked) KT.Broker:SetMinimap(checked) end) + minimap:SetPoint("TOPLEFT", purge, "BOTTOMLEFT", 0, -8) + + local disableDungeons = checkbox("Disable in dungeons (save CPU)", + "When this is checked, mob kills in dungeons won't be counted.", + function(_, checked) KT.Global.DISABLE_DUNGEONS = checked end) + disableDungeons:SetPoint("TOPLEFT", minimap, "BOTTOMLEFT", 0, -8) + + local disableRaids = checkbox("Disable in raids (save CPU)", + "When this is checked, mob kills in raids won't be counted.", + function(_, checked) KT.Global.DISABLE_RAIDS = checked end) + disableRaids:SetPoint("TOPLEFT", disableDungeons, "BOTTOMLEFT", 0, -8) + + local datetimeFormatDesc = self:CreateFontString(nil, "ARTWORK", "GameFontNormal") + datetimeFormatDesc:SetPoint("TOPLEFT", disableRaids, "BOTTOMLEFT", 0, -8) + datetimeFormatDesc:SetTextColor(1, 1, 1) + datetimeFormatDesc:SetText("Datetime format template (press enter to apply)") + + local datetimeFormat = CreateFrame("EditBox", "KillTrackOptDateTimeFormat", panel, "InputBoxTemplate") + datetimeFormat:SetHeight(22) + datetimeFormat:SetWidth(200) + datetimeFormat:SetPoint("LEFT", datetimeFormatDesc, "RIGHT", 8, 0) + datetimeFormat:SetAutoFocus(false) + datetimeFormat:EnableMouse(true) + local datetimeFormatPreview = self:CreateFontString(nil, "ARTWORK", "GameFontNormal") + datetimeFormatPreview:SetPoint("TOPLEFT", datetimeFormat, "BOTTOMLEFT", 0, -2) + datetimeFormatPreview:SetTextColor(1, 1, 1) + datetimeFormatPreview:SetText("Preview:") + local datetimeFormatPreviewValue = self:CreateFontString(nil, "ARTWORK", "GameFontNormal") + datetimeFormatPreviewValue:SetPoint("LEFT", datetimeFormatPreview, "RIGHT", 8, 0) + datetimeFormatPreviewValue:SetTextColor(1, 1, 1) + datetimeFormatPreviewValue:SetText(KTT:FormatDateTime() --[[@as string]]) + + datetimeFormat:SetScript("OnEditFocusGained", function(box) + box:SetTextColor(0, 1, 0) + box:HighlightText() + end) + local function setDateTimeFormat(box, enter) + box:SetTextColor(1, 1, 1) + local value = box:GetText() + if type(value) ~= "string" then + box:SetText(KT.Global.DATETIME_FORMAT) + box:HighlightText() + return + end + local valid, errMsg = pcall(KTT.FormatDateTime, KTT, nil, value) + if not valid then + KT:Msg("Invalid format string: " .. (errMsg or "unknown error")) + box:HighlightText() + return + end + KT.Global.DATETIME_FORMAT = value + if not enter then + KT:Msg("Updated datetime format!") + end + box:ClearFocus() + box:SetText(KT.Global.DATETIME_FORMAT) + end + datetimeFormat:SetScript("OnEditFocusLost", function(box) setDateTimeFormat(box) end) + datetimeFormat:SetScript("OnEnterPressed", function(box) setDateTimeFormat(box, true) end) + datetimeFormat:SetScript("OnTextChanged", function(box) + local value = box:GetText() + if type(value) ~= "string" then return end + local valid, result = pcall(KTT.FormatDateTime, KTT, nil, value) + if valid then + datetimeFormatPreviewValue:SetText(result --[[@as string]]) + else + datetimeFormatPreviewValue:SetText("invalid format") + end + end) + local datetimeFormatReset = button("Reset", "Reset the datetime format to the default", function() + KT.Global.DATETIME_FORMAT = KT.Defaults.DateTimeFormat + datetimeFormat:SetText(KT.Global.DATETIME_FORMAT) + end) + datetimeFormatReset:SetWidth(80) + datetimeFormatReset:SetPoint("LEFT", datetimeFormat, "RIGHT", 5, 0) + + local function init() + printKills:SetChecked(KT.Global.PRINTKILLS) + tooltipControl:SetChecked(KT.Global.TOOLTIP) + printNew:SetChecked(KT.Global.PRINTNEW) + countGroup:SetChecked(KT.Global.COUNT_GROUP) + threshold:SetText(tostring(KT.Global.ACHIEV_THRESHOLD)) + minimap:SetChecked(not KT.Global.BROKER.MINIMAP.hide) + disableDungeons:SetChecked(KT.Global.DISABLE_DUNGEONS) + disableRaids:SetChecked(KT.Global.DISABLE_RAIDS) + datetimeFormat:SetText(KT.Global.DATETIME_FORMAT) + end + + init() + + self:SetScript("OnShow", init) +end + +panel:SetScript("OnShow", function(self) Opt.Show(self) end) + +Settings.RegisterAddOnCategory(category) diff --git a/Timer.lua b/Timer.lua index 2d773b9..cb32f97 100644 --- a/Timer.lua +++ b/Timer.lua @@ -1,166 +1,166 @@ ---[[ - * Copyright (c) 2011-2020 by Adam Hellberg. - * - * This file is part of KillTrack. - * - * KillTrack is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KillTrack is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KillTrack. If not, see . ---]] - ----@class KillTrack -local KT = select(2, ...) - ----@class KillTrackTimer -local T = { - Time = { - Start = 0, - Stop = 0 - }, - Running = false, - ---@enum KillTrackTimerState - State = { - START = 0, - UPDATE = 1, - STOP = 2 - } -} - -KT.Timer = T - -local KTT = KT.Tools - ----@class KillTrackTimerData ----@field Last integer ----@field Current integer ----@field Start integer ----@field Stop integer ----@field Total integer ----@field Left integer ----@field LeftFormat string ----@field Progress number ----@field __DATA__ { [any]: any } -local TimerData = {} - ----@alias KillTrackTimerCallback fun(data: KillTrackTimerData, state: KillTrackTimerState) - -T.Frame = CreateFrame("Frame") - -local function TimeCheck(_, _) - if not T.Running then T.Frame:SetScript("OnUpdate", nil) return end - local now = time() - TimerData.Last = now - TimerData.Current = now - T.Time.Start - TimerData.Start = T.Time.Start - TimerData.Stop = T.Time.Stop - TimerData.Total = TimerData.Stop - TimerData.Start - TimerData.Left = TimerData.Total - TimerData.Current - TimerData.LeftFormat = KTT:FormatSeconds(TimerData.Left) - TimerData.Progress = TimerData.Current / TimerData.Total - T:RunCallback(T:GetAllData(), T.State.UPDATE) - if now >= T.Time.Stop then T:Stop() end -end - ----@return KillTrackTimerData -function T:GetAllData() - return KTT:TableCopy(TimerData) -end - ----@param key any ----@param failsafe boolean ----@return any -function T:GetData(key, failsafe) - local r - if failsafe then r = 0 end - if not TimerData.__DATA__ then if failsafe then return 0 else return nil end end - return TimerData.__DATA__[key] or r -end - ----@param key any ----@param value any -function T:SetData(key, value) - if type(TimerData.__DATA__) ~= "table" then TimerData.__DATA__ = {} end - TimerData.__DATA__[key] = value -end - ----@return boolean -function T:IsRunning() - return self.Running -end - ----@param seconds integer? ----@param minutes integer? ----@param hours integer? ----@param callback KillTrackTimerCallback? ----@param data { [any]: any }? ----@return boolean -function T:Start(seconds, minutes, hours, callback, data) - if self.Running then return false end - self.Running = true - self:Reset() - seconds = tonumber(seconds) or 0 - minutes = tonumber(minutes) or 0 - hours = tonumber(hours) or 0 - seconds = seconds + minutes * 60 + hours * 60 ^ 2 - if seconds <= 0 then - self.Running = false - KT:Msg("Time must be greater than zero.") - return false - end - if type(callback) == "function" then - self:SetCallback(callback) - end - if type(data) == "table" then - for k,v in pairs(data) do - self:SetData(k, v) - end - end - self.Time.Start = time() - self.Time.Stop = self.Time.Start + seconds - self:RunCallback(self:GetAllData(), self.State.START) - self.Frame:SetScript("OnUpdate", TimeCheck) - return true -end - -function T:Stop() - if not self.Running then return end - self.Frame:SetScript("OnUpdate", nil) - self:RunCallback(self:GetAllData(), self.State.STOP) - self.Running = false - self.Time.Diff = self.Time.Stop - self.Time.Start - return self.Time.Diff -end - -function T:Reset() - wipe(TimerData) - self.Time.Start = 0 - self.Time.Stop = 0 -end - ----@return KillTrackTimerCallback -function T:GetCallback() - return self.Callback -end - ----@param func KillTrackTimerCallback -function T:SetCallback(func) - if type(func) ~= "function" then error("Argument 'func' must be of type 'function'.") end - self.Callback = func -end - ----@param data KillTrackTimerData ----@param state KillTrackTimerState -function T:RunCallback(data, state) - if type(data) ~= "table" then error("Argument 'data' must be of type 'table'.") end - local callback = self:GetCallback() - if callback then callback(data, state) end -end +--[[ + * Copyright (c) 2011-2020 by Adam Hellberg. + * + * This file is part of KillTrack. + * + * KillTrack is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KillTrack is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KillTrack. If not, see . +--]] + +---@class KillTrack +local KT = select(2, ...) + +---@class KillTrackTimer +local T = { + Time = { + Start = 0, + Stop = 0 + }, + Running = false, + ---@enum KillTrackTimerState + State = { + START = 0, + UPDATE = 1, + STOP = 2 + } +} + +KT.Timer = T + +local KTT = KT.Tools + +---@class KillTrackTimerData +---@field Last integer +---@field Current integer +---@field Start integer +---@field Stop integer +---@field Total integer +---@field Left integer +---@field LeftFormat string +---@field Progress number +---@field __DATA__ { [any]: any } +local TimerData = {} + +---@alias KillTrackTimerCallback fun(data: KillTrackTimerData, state: KillTrackTimerState) + +T.Frame = CreateFrame("Frame") + +local function TimeCheck(_, _) + if not T.Running then T.Frame:SetScript("OnUpdate", nil) return end + local now = time() + TimerData.Last = now + TimerData.Current = now - T.Time.Start + TimerData.Start = T.Time.Start + TimerData.Stop = T.Time.Stop + TimerData.Total = TimerData.Stop - TimerData.Start + TimerData.Left = TimerData.Total - TimerData.Current + TimerData.LeftFormat = KTT:FormatSeconds(TimerData.Left) + TimerData.Progress = TimerData.Current / TimerData.Total + T:RunCallback(T:GetAllData(), T.State.UPDATE) + if now >= T.Time.Stop then T:Stop() end +end + +---@return KillTrackTimerData +function T:GetAllData() + return KTT:TableCopy(TimerData) +end + +---@param key any +---@param failsafe boolean +---@return any +function T:GetData(key, failsafe) + local r + if failsafe then r = 0 end + if not TimerData.__DATA__ then if failsafe then return 0 else return nil end end + return TimerData.__DATA__[key] or r +end + +---@param key any +---@param value any +function T:SetData(key, value) + if type(TimerData.__DATA__) ~= "table" then TimerData.__DATA__ = {} end + TimerData.__DATA__[key] = value +end + +---@return boolean +function T:IsRunning() + return self.Running +end + +---@param seconds integer? +---@param minutes integer? +---@param hours integer? +---@param callback KillTrackTimerCallback? +---@param data { [any]: any }? +---@return boolean +function T:Start(seconds, minutes, hours, callback, data) + if self.Running then return false end + self.Running = true + self:Reset() + seconds = tonumber(seconds) or 0 + minutes = tonumber(minutes) or 0 + hours = tonumber(hours) or 0 + seconds = seconds + minutes * 60 + hours * 60 ^ 2 + if seconds <= 0 then + self.Running = false + KT:Msg("Time must be greater than zero.") + return false + end + if type(callback) == "function" then + self:SetCallback(callback) + end + if type(data) == "table" then + for k,v in pairs(data) do + self:SetData(k, v) + end + end + self.Time.Start = time() + self.Time.Stop = self.Time.Start + seconds + self:RunCallback(self:GetAllData(), self.State.START) + self.Frame:SetScript("OnUpdate", TimeCheck) + return true +end + +function T:Stop() + if not self.Running then return end + self.Frame:SetScript("OnUpdate", nil) + self:RunCallback(self:GetAllData(), self.State.STOP) + self.Running = false + self.Time.Diff = self.Time.Stop - self.Time.Start + return self.Time.Diff +end + +function T:Reset() + wipe(TimerData) + self.Time.Start = 0 + self.Time.Stop = 0 +end + +---@return KillTrackTimerCallback +function T:GetCallback() + return self.Callback +end + +---@param func KillTrackTimerCallback +function T:SetCallback(func) + if type(func) ~= "function" then error("Argument 'func' must be of type 'function'.") end + self.Callback = func +end + +---@param data KillTrackTimerData +---@param state KillTrackTimerState +function T:RunCallback(data, state) + if type(data) ~= "table" then error("Argument 'data' must be of type 'table'.") end + local callback = self:GetCallback() + if callback then callback(data, state) end +end diff --git a/TimerFrame.lua b/TimerFrame.lua index db7194d..9a0a6bf 100644 --- a/TimerFrame.lua +++ b/TimerFrame.lua @@ -1,216 +1,216 @@ ---[[ - * Copyright (c) 2011-2020 by Adam Hellberg. - * - * This file is part of KillTrack. - * - * KillTrack is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KillTrack is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KillTrack. If not, see . ---]] - ----@class KillTrack -local KT = select(2, ...) - ----@class KillTrackTimerFrame -local TF = { - Running = false -} - -KT.TimerFrame = TF - -local T = KT.Timer - -local function Enabled(object, enabled) - if not object.Enable or not object.Disable then return end - if enabled then - object:Enable() - else - object:Disable() - end -end - -local frame - -local function SetupFrame() - if frame then return end - frame = CreateFrame("Frame", nil, UIParent, BackdropTemplateMixin and "BackdropTemplate") - frame:Hide() - frame:EnableMouse(true) - frame:SetMovable(true) - frame:SetWidth(200) - frame:SetHeight(93) - - frame:SetPoint("CENTER") - - local bd = { - bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", - edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", - tile = true, - edgeSize = 16, - tileSize = 32, - insets = { - left = 2.5, - right = 2.5, - top = 2.5, - bottom = 2.5 - } - } - - frame:SetBackdrop(bd) - - frame:SetScript("OnMouseDown", function(f) f:StartMoving() end) - frame:SetScript("OnMouseUp", function(f) f:StopMovingOrSizing() end) - - ---@diagnostic disable-next-line: inject-field - frame.currentLabel = frame:CreateFontString(nil, "OVERLAY", nil) - frame.currentLabel:SetFont("Fonts\\FRIZQT__.TTF", 10, nil) - frame.currentLabel:SetPoint("TOPLEFT", frame, "TOPLEFT", 6, -6) - frame.currentLabel:SetText("Number of kills:") - - ---@diagnostic disable-next-line: inject-field - frame.currentCount = frame:CreateFontString(nil, "OVERLAY", nil) - frame.currentCount:SetFont("Fonts\\FRIZQT__.TTF", 10, nil) - frame.currentCount:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -6, -6) - frame.currentCount:SetText("0") - - ---@diagnostic disable-next-line: inject-field - frame.timeLabel = frame:CreateFontString(nil, "OVERLAY", nil) - frame.timeLabel:SetFont("Fonts\\FRIZQT__.TTF", 10, nil) - frame.timeLabel:SetPoint("TOPLEFT", frame.currentLabel, "BOTTOMLEFT", 0, -2) - frame.timeLabel:SetText("Time left:") - - ---@diagnostic disable-next-line: inject-field - frame.timeCount = frame:CreateFontString(nil, "OVERLAY", nil) - frame.timeCount:SetFont("Fonts\\FRIZQT__.TTF", 10, nil) - frame.timeCount:SetPoint("TOPRIGHT", frame.currentCount, "BOTTOMRIGHT", 0, -2) - frame.timeCount:SetText("00:00:00") - - ---@diagnostic disable-next-line: inject-field - frame.killsPerMinuteLabel = frame:CreateFontString(nil, "OVERLAY", nil) - frame.killsPerMinuteLabel:SetFont("Fonts\\FRIZQT__.TTF", 10, nil) - frame.killsPerMinuteLabel:SetPoint("TOPLEFT", frame.timeLabel, "BOTTOMLEFT", 0, -2) - frame.killsPerMinuteLabel:SetText("Kills Per Minute:") - - ---@diagnostic disable-next-line: inject-field - frame.killsPerMinuteCount = frame:CreateFontString(nil, "OVERLAY", nil) - frame.killsPerMinuteCount:SetFont("Fonts\\FRIZQT__.TTF", 10, nil) - frame.killsPerMinuteCount:SetPoint("TOPRIGHT", frame.timeCount, "BOTTOMRIGHT", 0, -2) - frame.killsPerMinuteCount:SetText("0") - - ---@diagnostic disable-next-line: inject-field - frame.killsPerSecondLabel = frame:CreateFontString(nil, "OVERLAY", nil) - frame.killsPerSecondLabel:SetFont("Fonts\\FRIZQT__.TTF", 10, nil) - frame.killsPerSecondLabel:SetPoint("TOPLEFT", frame.killsPerMinuteLabel, "BOTTOMLEFT", 0, -2) - frame.killsPerSecondLabel:SetText("Kills Per Second:") - - ---@diagnostic disable-next-line: inject-field - frame.killsPerSecondCount = frame:CreateFontString(nil, "OVERLAY", nil) - frame.killsPerSecondCount:SetFont("Fonts\\FRIZQT__.TTF", 10, nil) - frame.killsPerSecondCount:SetPoint("TOPRIGHT", frame.killsPerMinuteCount, "BOTTOMRIGHT", 0, -2) - frame.killsPerSecondCount:SetText("Kills Per Minute:") - - ---@diagnostic disable-next-line: inject-field - frame.cancelButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") - frame.cancelButton:SetSize(60, 16) - frame.cancelButton:SetPoint("BOTTOM", frame, "BOTTOM", -40, 7) - frame.cancelButton:SetScript("OnLoad", function(self) self:Disable() end) - frame.cancelButton:SetScript("OnClick", function() TF:Cancel() end) - frame.cancelButton:SetText("Stop") - - ---@diagnostic disable-next-line: inject-field - frame.closeButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") - frame.closeButton:SetSize(60, 16) - frame.closeButton:SetPoint("BOTTOM", frame, "BOTTOM", 40, 7) - frame.closeButton:SetScript("OnLoad", function(self) self:Disable() end) - frame.closeButton:SetScript("OnClick", function() TF:Close() end) - frame.closeButton:SetText("Close") - - ---@diagnostic disable-next-line: inject-field - frame.progressBar = CreateFrame("StatusBar", nil, frame) - frame.progressBar:SetStatusBarTexture([[Interface\TargetingFrame\UI-StatusBar]]) - frame.progressBar:SetStatusBarColor(0, 1, 0) - frame.progressBar:SetMinMaxValues(0, 1) - frame.progressBar:SetValue(0) - frame.progressBar:SetPoint("TOPLEFT", frame.killsPerSecondLabel, "BOTTOMLEFT", -1, -2) - frame.progressBar:SetPoint("RIGHT", frame.killsPerSecondCount, "RIGHT", 0, 0) - frame.progressBar:SetPoint("BOTTOM", frame.cancelButton, "TOP", 0, 2) - - ---@diagnostic disable-next-line: inject-field - frame.progressLabel = frame.progressBar:CreateFontString(nil, "OVERLAY", nil) - frame.progressLabel:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE") - frame.progressLabel:SetAllPoints(frame.progressBar) - frame.progressLabel:SetText("0%") -end - -function TF:InitializeControls() - frame.currentCount:SetText("0") - frame.timeCount:SetText("00:00:00") - frame.progressLabel:SetText("0%") - frame.progressBar:SetValue(0) - self:UpdateControls() -end - -function TF:UpdateControls() - Enabled(frame.cancelButton, self.Running) - Enabled(frame.closeButton, not self.Running) -end - -function TF:UpdateData(data, state) - if state == T.State.START then - self:InitializeControls() - else - local kills = T:GetData("Kills", true) - local kpm, kps - if data.Current <= 0 then - kpm, kps = 0, 0 - else - kpm = kills / (data.Current / 60) - kps = kills / data.Current - end - frame.currentCount:SetText(kills) - frame.timeCount:SetText(data.LeftFormat) - frame.progressLabel:SetText(floor(data.Progress*100) .. "%") - frame.progressBar:SetMinMaxValues(0, data.Total) - frame.progressBar:SetValue(data.Current) - frame.killsPerMinuteCount:SetText(("%.2f"):format(kpm)) - frame.killsPerSecondCount:SetText(("%.2f"):format(kps)) - if state == T.State.STOP then self:Stop() end - end - self:UpdateControls() -end - -function TF:Start(s, m, h) - if self.Running then return end - self.Running = true - SetupFrame() - self:InitializeControls() - frame:Show() - if not T:Start(s, m, h, function(d, u) TF:UpdateData(d, u) end, nil) then - self:Stop() - frame:Hide() - end -end - -function TF:Stop() - if not self.Running then return end - self.Running = false -end - -function TF:Cancel() - T:Stop() -end - -function TF:Close() - if not frame then return end - self:InitializeControls() - frame:Hide() -end +--[[ + * Copyright (c) 2011-2020 by Adam Hellberg. + * + * This file is part of KillTrack. + * + * KillTrack is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KillTrack is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KillTrack. If not, see . +--]] + +---@class KillTrack +local KT = select(2, ...) + +---@class KillTrackTimerFrame +local TF = { + Running = false +} + +KT.TimerFrame = TF + +local T = KT.Timer + +local function Enabled(object, enabled) + if not object.Enable or not object.Disable then return end + if enabled then + object:Enable() + else + object:Disable() + end +end + +local frame + +local function SetupFrame() + if frame then return end + frame = CreateFrame("Frame", nil, UIParent, BackdropTemplateMixin and "BackdropTemplate") + frame:Hide() + frame:EnableMouse(true) + frame:SetMovable(true) + frame:SetWidth(200) + frame:SetHeight(93) + + frame:SetPoint("CENTER") + + local bd = { + bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, + edgeSize = 16, + tileSize = 32, + insets = { + left = 2.5, + right = 2.5, + top = 2.5, + bottom = 2.5 + } + } + + frame:SetBackdrop(bd) + + frame:SetScript("OnMouseDown", function(f) f:StartMoving() end) + frame:SetScript("OnMouseUp", function(f) f:StopMovingOrSizing() end) + + ---@diagnostic disable-next-line: inject-field + frame.currentLabel = frame:CreateFontString(nil, "OVERLAY", nil) + frame.currentLabel:SetFont("Fonts\\FRIZQT__.TTF", 10, nil) + frame.currentLabel:SetPoint("TOPLEFT", frame, "TOPLEFT", 6, -6) + frame.currentLabel:SetText("Number of kills:") + + ---@diagnostic disable-next-line: inject-field + frame.currentCount = frame:CreateFontString(nil, "OVERLAY", nil) + frame.currentCount:SetFont("Fonts\\FRIZQT__.TTF", 10, nil) + frame.currentCount:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -6, -6) + frame.currentCount:SetText("0") + + ---@diagnostic disable-next-line: inject-field + frame.timeLabel = frame:CreateFontString(nil, "OVERLAY", nil) + frame.timeLabel:SetFont("Fonts\\FRIZQT__.TTF", 10, nil) + frame.timeLabel:SetPoint("TOPLEFT", frame.currentLabel, "BOTTOMLEFT", 0, -2) + frame.timeLabel:SetText("Time left:") + + ---@diagnostic disable-next-line: inject-field + frame.timeCount = frame:CreateFontString(nil, "OVERLAY", nil) + frame.timeCount:SetFont("Fonts\\FRIZQT__.TTF", 10, nil) + frame.timeCount:SetPoint("TOPRIGHT", frame.currentCount, "BOTTOMRIGHT", 0, -2) + frame.timeCount:SetText("00:00:00") + + ---@diagnostic disable-next-line: inject-field + frame.killsPerMinuteLabel = frame:CreateFontString(nil, "OVERLAY", nil) + frame.killsPerMinuteLabel:SetFont("Fonts\\FRIZQT__.TTF", 10, nil) + frame.killsPerMinuteLabel:SetPoint("TOPLEFT", frame.timeLabel, "BOTTOMLEFT", 0, -2) + frame.killsPerMinuteLabel:SetText("Kills Per Minute:") + + ---@diagnostic disable-next-line: inject-field + frame.killsPerMinuteCount = frame:CreateFontString(nil, "OVERLAY", nil) + frame.killsPerMinuteCount:SetFont("Fonts\\FRIZQT__.TTF", 10, nil) + frame.killsPerMinuteCount:SetPoint("TOPRIGHT", frame.timeCount, "BOTTOMRIGHT", 0, -2) + frame.killsPerMinuteCount:SetText("0") + + ---@diagnostic disable-next-line: inject-field + frame.killsPerSecondLabel = frame:CreateFontString(nil, "OVERLAY", nil) + frame.killsPerSecondLabel:SetFont("Fonts\\FRIZQT__.TTF", 10, nil) + frame.killsPerSecondLabel:SetPoint("TOPLEFT", frame.killsPerMinuteLabel, "BOTTOMLEFT", 0, -2) + frame.killsPerSecondLabel:SetText("Kills Per Second:") + + ---@diagnostic disable-next-line: inject-field + frame.killsPerSecondCount = frame:CreateFontString(nil, "OVERLAY", nil) + frame.killsPerSecondCount:SetFont("Fonts\\FRIZQT__.TTF", 10, nil) + frame.killsPerSecondCount:SetPoint("TOPRIGHT", frame.killsPerMinuteCount, "BOTTOMRIGHT", 0, -2) + frame.killsPerSecondCount:SetText("Kills Per Minute:") + + ---@diagnostic disable-next-line: inject-field + frame.cancelButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") + frame.cancelButton:SetSize(60, 16) + frame.cancelButton:SetPoint("BOTTOM", frame, "BOTTOM", -40, 7) + frame.cancelButton:SetScript("OnLoad", function(self) self:Disable() end) + frame.cancelButton:SetScript("OnClick", function() TF:Cancel() end) + frame.cancelButton:SetText("Stop") + + ---@diagnostic disable-next-line: inject-field + frame.closeButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") + frame.closeButton:SetSize(60, 16) + frame.closeButton:SetPoint("BOTTOM", frame, "BOTTOM", 40, 7) + frame.closeButton:SetScript("OnLoad", function(self) self:Disable() end) + frame.closeButton:SetScript("OnClick", function() TF:Close() end) + frame.closeButton:SetText("Close") + + ---@diagnostic disable-next-line: inject-field + frame.progressBar = CreateFrame("StatusBar", nil, frame) + frame.progressBar:SetStatusBarTexture([[Interface\TargetingFrame\UI-StatusBar]]) + frame.progressBar:SetStatusBarColor(0, 1, 0) + frame.progressBar:SetMinMaxValues(0, 1) + frame.progressBar:SetValue(0) + frame.progressBar:SetPoint("TOPLEFT", frame.killsPerSecondLabel, "BOTTOMLEFT", -1, -2) + frame.progressBar:SetPoint("RIGHT", frame.killsPerSecondCount, "RIGHT", 0, 0) + frame.progressBar:SetPoint("BOTTOM", frame.cancelButton, "TOP", 0, 2) + + ---@diagnostic disable-next-line: inject-field + frame.progressLabel = frame.progressBar:CreateFontString(nil, "OVERLAY", nil) + frame.progressLabel:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE") + frame.progressLabel:SetAllPoints(frame.progressBar) + frame.progressLabel:SetText("0%") +end + +function TF:InitializeControls() + frame.currentCount:SetText("0") + frame.timeCount:SetText("00:00:00") + frame.progressLabel:SetText("0%") + frame.progressBar:SetValue(0) + self:UpdateControls() +end + +function TF:UpdateControls() + Enabled(frame.cancelButton, self.Running) + Enabled(frame.closeButton, not self.Running) +end + +function TF:UpdateData(data, state) + if state == T.State.START then + self:InitializeControls() + else + local kills = T:GetData("Kills", true) + local kpm, kps + if data.Current <= 0 then + kpm, kps = 0, 0 + else + kpm = kills / (data.Current / 60) + kps = kills / data.Current + end + frame.currentCount:SetText(kills) + frame.timeCount:SetText(data.LeftFormat) + frame.progressLabel:SetText(floor(data.Progress*100) .. "%") + frame.progressBar:SetMinMaxValues(0, data.Total) + frame.progressBar:SetValue(data.Current) + frame.killsPerMinuteCount:SetText(("%.2f"):format(kpm)) + frame.killsPerSecondCount:SetText(("%.2f"):format(kps)) + if state == T.State.STOP then self:Stop() end + end + self:UpdateControls() +end + +function TF:Start(s, m, h) + if self.Running then return end + self.Running = true + SetupFrame() + self:InitializeControls() + frame:Show() + if not T:Start(s, m, h, function(d, u) TF:UpdateData(d, u) end, nil) then + self:Stop() + frame:Hide() + end +end + +function TF:Stop() + if not self.Running then return end + self.Running = false +end + +function TF:Cancel() + T:Stop() +end + +function TF:Close() + if not frame then return end + self:InitializeControls() + frame:Hide() +end diff --git a/Tools.lua b/Tools.lua index 7d022aa..d88f553 100644 --- a/Tools.lua +++ b/Tools.lua @@ -1,146 +1,146 @@ ---[[ - * Copyright (c) 2011-2020 by Adam Hellberg. - * - * This file is part of KillTrack. - * - * KillTrack is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KillTrack is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KillTrack. If not, see . ---]] - ----@class KillTrack -local KT = select(2, ...) - ----@class KillTrackTools -local KTT = {} - -KT.Tools = KTT - ------------------- --- NUMBER TOOLS -- ------------------- - ----@param seconds number ----@return string -function KTT:FormatSeconds(seconds) - local hours = floor(seconds / 3600) - local minutes = floor(seconds / 60) - hours * 60 - seconds = seconds - minutes * 60 - hours * 3600 - return ("%02d:%02d:%02d"):format(hours, minutes, seconds) -end - ------------------- --- STRING TOOLS -- ------------------- - ----@param s string ----@return string -function KTT:Trim(s) - return (s:gsub("^%s*(.-)%s*$", "%1")) -end - ----@param s string ----@return string[] -function KTT:Split(s) - local r = {} - local tokpat = "%S+" - local spat = [=[^(['"])]=] - local epat = [=[(['"])$]=] - local escpat = [=[(\*)['"]$]=] - local buf, quoted - for token in string.gmatch(s, tokpat) do - local squoted = token:match(spat) - local equoted = token:match(epat) - local escaped = token:match(escpat) - if squoted and not quoted and not equoted then - buf, quoted = token, squoted - elseif buf and equoted == quoted and #escaped % 2 == 0 then - token, buf, quoted = buf .. " " .. token, nil, nil - elseif buf then - buf = buf .. " " .. token - end - if not buf then - r[#r + 1] = token:gsub(spat, ""):gsub(epat, ""):gsub([[\(.)]], "%1") - end - end - if buf then - r[#r + 1] = buf - end - return r -end - ------------------ --- TABLE TOOLS -- ------------------ - ----@param tbl table ----@param val any ----@return boolean -function KTT:InTable(tbl, val) - for _,v in pairs(tbl) do - if v == val then return true end - end - return false -end - ----@param tbl table ----@param cache table? ----@return table -function KTT:TableCopy(tbl, cache) - if type(tbl) ~= "table" then return tbl end - cache = cache or {} - if cache[tbl] then return cache[tbl] end - local copy = {} - cache[tbl] = copy - for k, v in pairs(tbl) do - copy[self:TableCopy(k, cache)] = self:TableCopy(v, cache) - end - return copy -end - ----@param table table ----@return integer -function KTT:TableLength(table) - local count = 0 - for _, _ in pairs(table) do - count = count + 1 - end - return count -end - --------------------- --- DATETIME TOOLS -- --------------------- - ----@param timestamp number? ----@param format string? ----@return string|osdate -function KTT:FormatDateTime(timestamp, format) - timestamp = timestamp or GetServerTime() - format = format or KT.Global.DATETIME_FORMAT or KT.Defaults.DateTimeFormat - return date(format, timestamp) -end - ------------------ --- OTHER TOOLS -- ------------------ - -local ssplit = strsplit - ----@param guid string? ----@return integer? -function KTT.GUIDToID(guid) - if not guid then return nil end - -- local id = guid:match("^%w+%-0%-%d+%-%d+%-%d+%-(%d+)%-[A-Z%d]+$") - local _, _, _, _, _, id = ssplit("-", guid) - return tonumber(id) -end +--[[ + * Copyright (c) 2011-2020 by Adam Hellberg. + * + * This file is part of KillTrack. + * + * KillTrack is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KillTrack is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KillTrack. If not, see . +--]] + +---@class KillTrack +local KT = select(2, ...) + +---@class KillTrackTools +local KTT = {} + +KT.Tools = KTT + +------------------ +-- NUMBER TOOLS -- +------------------ + +---@param seconds number +---@return string +function KTT:FormatSeconds(seconds) + local hours = floor(seconds / 3600) + local minutes = floor(seconds / 60) - hours * 60 + seconds = seconds - minutes * 60 - hours * 3600 + return ("%02d:%02d:%02d"):format(hours, minutes, seconds) +end + +------------------ +-- STRING TOOLS -- +------------------ + +---@param s string +---@return string +function KTT:Trim(s) + return (s:gsub("^%s*(.-)%s*$", "%1")) +end + +---@param s string +---@return string[] +function KTT:Split(s) + local r = {} + local tokpat = "%S+" + local spat = [=[^(['"])]=] + local epat = [=[(['"])$]=] + local escpat = [=[(\*)['"]$]=] + local buf, quoted + for token in string.gmatch(s, tokpat) do + local squoted = token:match(spat) + local equoted = token:match(epat) + local escaped = token:match(escpat) + if squoted and not quoted and not equoted then + buf, quoted = token, squoted + elseif buf and equoted == quoted and #escaped % 2 == 0 then + token, buf, quoted = buf .. " " .. token, nil, nil + elseif buf then + buf = buf .. " " .. token + end + if not buf then + r[#r + 1] = token:gsub(spat, ""):gsub(epat, ""):gsub([[\(.)]], "%1") + end + end + if buf then + r[#r + 1] = buf + end + return r +end + +----------------- +-- TABLE TOOLS -- +----------------- + +---@param tbl table +---@param val any +---@return boolean +function KTT:InTable(tbl, val) + for _,v in pairs(tbl) do + if v == val then return true end + end + return false +end + +---@param tbl table +---@param cache table? +---@return table +function KTT:TableCopy(tbl, cache) + if type(tbl) ~= "table" then return tbl end + cache = cache or {} + if cache[tbl] then return cache[tbl] end + local copy = {} + cache[tbl] = copy + for k, v in pairs(tbl) do + copy[self:TableCopy(k, cache)] = self:TableCopy(v, cache) + end + return copy +end + +---@param table table +---@return integer +function KTT:TableLength(table) + local count = 0 + for _, _ in pairs(table) do + count = count + 1 + end + return count +end + +-------------------- +-- DATETIME TOOLS -- +-------------------- + +---@param timestamp number? +---@param format string? +---@return string|osdate +function KTT:FormatDateTime(timestamp, format) + timestamp = timestamp or GetServerTime() + format = format or KT.Global.DATETIME_FORMAT or KT.Defaults.DateTimeFormat + return date(format, timestamp) +end + +----------------- +-- OTHER TOOLS -- +----------------- + +local ssplit = strsplit + +---@param guid string? +---@return integer? +function KTT.GUIDToID(guid) + if not guid then return nil end + -- local id = guid:match("^%w+%-0%-%d+%-%d+%-%d+%-(%d+)%-[A-Z%d]+$") + local _, _, _, _, _, id = ssplit("-", guid) + return tonumber(id) +end