diff --git a/.github/screenshots/swingtimer.png b/.github/screenshots/swingtimer.png new file mode 100644 index 000000000..935d6372e Binary files /dev/null and b/.github/screenshots/swingtimer.png differ diff --git a/EUI_MacroFactory.lua b/EUI_MacroFactory.lua index f24cb980d..3a05ef1dd 100644 --- a/EUI_MacroFactory.lua +++ b/EUI_MacroFactory.lua @@ -618,10 +618,10 @@ function EllesmereUI.BuildMacroFactory(parent, startY, PP) } -- Detect current spec and class - local specIndex = GetSpecialization() + local specIndex = C_SpecializationInfo.GetSpecialization() local activeSpecID, activeSpecName if specIndex then - activeSpecID, activeSpecName = GetSpecializationInfo(specIndex) + activeSpecID, activeSpecName = C_SpecializationInfo.GetSpecializationInfo(specIndex) end local activeClassName = UnitClass("player") or "Unknown" -- All spec macro bodies use spell-ID {n} tokens (localized at build time via @@ -1681,8 +1681,8 @@ function EllesmereUI.RefreshMacroFactory() if not mf or not mf.parent or not mf.parent.IsObjectType then return end -- PLAYER_SPECIALIZATION_CHANGED can fire several times for one switch; skip -- the rebuild if the spec that's already built hasn't actually changed. - local idx = GetSpecialization() - local curSpecID = idx and GetSpecializationInfo(idx) or nil + local idx = C_SpecializationInfo.GetSpecialization() + local curSpecID = idx and C_SpecializationInfo.GetSpecializationInfo(idx) or nil if curSpecID == mf.builtSpecID then return end local oldContainer = mf.container diff --git a/EllesmereUIBags/EllesmereUIBags.lua b/EllesmereUIBags/EllesmereUIBags.lua index 1f0a073a7..cc0adb5b1 100644 --- a/EllesmereUIBags/EllesmereUIBags.lua +++ b/EllesmereUIBags/EllesmereUIBags.lua @@ -22,6 +22,10 @@ EUI_BagsWindow = CreateFrame("Frame", "EUI_BagsWindowFrame", UIParent) EUI_BagsWindow:Hide() local SLOT_SIZE, SPACING = 34, 4 +local GetItemInfo = C_Item.GetItemInfo +local GetItemInfoInstant = C_Item.GetItemInfoInstant +local GetItemQualityColor = C_Item.GetItemQualityColor +local IsEquippableItem = C_Item.IsEquippableItem -- Red-tint usability test (shared with the bank module via EUI). Tooltip must come from -- the real item, never GetItemByID: scaling gear's bonus IDs lower its required level, but diff --git a/EllesmereUIBags/EllesmereUIBags_Bank.lua b/EllesmereUIBags/EllesmereUIBags_Bank.lua index 58b5704de..c6cad186f 100644 --- a/EllesmereUIBags/EllesmereUIBags_Bank.lua +++ b/EllesmereUIBags/EllesmereUIBags_Bank.lua @@ -5,6 +5,9 @@ if EUI_CLIENT_BLOCKED then return end -- pre-12.1 client failsafe (EllesmereUI_C -- Visually matches the Bags module with sidebar, search, and sorting ------------------------------------------------------------------------------- local EUI = EllesmereUI +local GetItemInfo = C_Item.GetItemInfo +local GetItemInfoInstant = C_Item.GetItemInfoInstant +local GetItemQualityColor = C_Item.GetItemQualityColor -- Profile access helper (DB created in EUI_Bags_Options.lua, loaded first per TOC) local _emptyP = {} local function BP() return (EUI._bagsDB and EUI._bagsDB.profile) or _emptyP end diff --git a/EllesmereUIBags/EllesmereUIBags_Categories.lua b/EllesmereUIBags/EllesmereUIBags_Categories.lua index e2e944125..f7ba9be3d 100644 --- a/EllesmereUIBags/EllesmereUIBags_Categories.lua +++ b/EllesmereUIBags/EllesmereUIBags_Categories.lua @@ -9,6 +9,7 @@ if EUI_CLIENT_BLOCKED then return end -- pre-12.1 client failsafe (EllesmereUI_C local CategoryManager = {} -- Profile access helper (DB created in EUI_Bags_Options.lua, loaded first per TOC) local EUI = EllesmereUI +local GetItemInfoInstant = C_Item.GetItemInfoInstant local _emptyP = {} local function BP() return (EUI._bagsDB and EUI._bagsDB.profile) or _emptyP end diff --git a/EllesmereUIBlizzardSkin/EllesmereUIBlizzardSkin_WindowEngine.lua b/EllesmereUIBlizzardSkin/EllesmereUIBlizzardSkin_WindowEngine.lua index 76848a092..9a991062c 100644 --- a/EllesmereUIBlizzardSkin/EllesmereUIBlizzardSkin_WindowEngine.lua +++ b/EllesmereUIBlizzardSkin/EllesmereUIBlizzardSkin_WindowEngine.lua @@ -1191,9 +1191,10 @@ function WSkin.Tab(tab, opts) hooksecurefunc(sys, "SetTabVisuallySelected", UpdateAllTabs) end end + -- Icon tabs carry their label in Icon; retain its clipping mask as well. for j = 1, select("#", tab:GetRegions()) do local r = select(j, tab:GetRegions()) - if r and r:IsObjectType("Texture") then + if r and r ~= tab.Icon and r ~= tab.IconMask and r:IsObjectType("Texture") then r:SetTexture("") if r.SetAtlas then r:SetAtlas("") end end diff --git a/EllesmereUIOptions/EUI_QoL_Options.lua b/EllesmereUIOptions/EUI_QoL_Options.lua index 13a06d2e7..c06fe39f9 100644 --- a/EllesmereUIOptions/EUI_QoL_Options.lua +++ b/EllesmereUIOptions/EUI_QoL_Options.lua @@ -2814,10 +2814,12 @@ initFrame:SetScript("OnEvent", function(self) return math.abs(y) end + local pages = { PAGE_QOL, PAGE_RAIDTOOLS, PAGE_CURSOR, PAGE_SHIFTER, PAGE_MOVEMENT, PAGE_UPGCALC } + if _G._EUI_Swing_Profile then pages[#pages + 1] = "Swing Timer" end EllesmereUI:RegisterModule("EllesmereUIQoL", { title = "Quality of Life", description = "Quality of life features and custom cursor.", - pages = { PAGE_QOL, PAGE_RAIDTOOLS, PAGE_CURSOR, PAGE_SHIFTER, PAGE_MOVEMENT, PAGE_UPGCALC }, + pages = pages, searchTerms = { "brez", "bres", "battle res", "combat res", "cursor", "macro", "fps", "logging", "combat log", "warcraft logs", "upgrade", "ilvl", "item level", "crest", "upgrade calculator", "shifter", "move", "drag", "position", "demodal", "drift", "combat alert", "enter combat", "leave combat", "in combat", "combat text", "combat notification", "transform", "transforms", "costume", "disguise", "chef's hat", "noggenfogger", "target distance", "distance to target", "range text", "yard", "yards", "movement", "mobility", "gap closer", "blink", "gateway", "warlock gateway", "control shard", "time spiral", "free movement", "raid tools", "raid", "pull timer", "pull", "ready check", "role check", "raid marker", "target marker", "world marker", "flare", "disband", "convert to raid", "countdown" }, buildPage = function(pageName, parent, yOffset) -- The Raid Tools settings preview ends when any OTHER QoL page @@ -2828,6 +2830,9 @@ initFrame:SetScript("OnEvent", function(self) and _G._EUI_RaidTools_Preview then _G._EUI_RaidTools_Preview(false) end + if pageName == "Swing Timer" and _G._EUI_BuildSwingPage then + return _G._EUI_BuildSwingPage(pageName, parent, yOffset) + end if pageName == PAGE_QOL then return BuildQoLPage(pageName, parent, yOffset) end diff --git a/EllesmereUIOptions/EUI_QoL_Swing_Options.lua b/EllesmereUIOptions/EUI_QoL_Swing_Options.lua new file mode 100644 index 000000000..33b798a0d --- /dev/null +++ b/EllesmereUIOptions/EUI_QoL_Swing_Options.lua @@ -0,0 +1,47 @@ +if EUI_CLIENT_BLOCKED then return end +if not EllesmereUI._ModuleNS["EllesmereUIQoL"] or not _G._EUI_Swing_Profile then return end + +_G._EUI_BuildSwingPage = function(_, parent, yOffset) + local EUI = EllesmereUI + local W = EUI.Widgets + local y = yOffset + EUI:ClearContentHeader() + local function Get(key) return _G._EUI_Swing_Profile()[key] end + local function Set(key, value) + _G._EUI_Swing_Profile()[key] = value + _G._EUI_Swing_Apply() + end + local function Toggle(label, key) + return { type = "toggle", text = label, + getValue = function() return Get(key) end, + setValue = function(v) Set(key, v) end } + end + local function Slider(label, key, lo, hi) + return { type = "slider", text = label, min = lo, max = hi, step = 1, + getValue = function() return Get(key) end, + setValue = function(v) Set(key, v) end } + end + local _, h = W:DualRow(parent, y, + Toggle("Enable Swing Timer", "enabled"), Toggle("Only Show In Combat", "combatOnly")) + y = y - h + _, h = W:DualRow(parent, y, + Toggle("Main Hand", "mainHand"), Toggle("Off Hand", "offHand")) + y = y - h + _, h = W:DualRow(parent, y, + Toggle("Ranged", "ranged"), Toggle("Highlight Queued Attacks", "queueColor")) + y = y - h + _, h = W:DualRow(parent, y, + Slider("Width", "width", 100, 600), Slider("Bar Height", "height", 12, 40)) + y = y - h + _, h = W:DualRow(parent, y, + Slider("Bar Spacing", "gap", 0, 20), Slider("Text Size", "fontSize", 8, 24)) + y = y - h + local _, textureNames, textureOrder = EUI.BuildBarTextureTables() + _, h = W:DualRow(parent, y, + { type = "dropdown", text = "Bar Texture", values = textureNames, order = textureOrder, + getValue = function() return Get("texture") end, + setValue = function(v) Set("texture", v) end }, + { type = "label", text = "Position with Unlock Mode" }) + y = y - h + return math.abs(y) +end diff --git a/EllesmereUIOptions/EllesmereUIOptions.toc b/EllesmereUIOptions/EllesmereUIOptions.toc index 7270cfa86..9b4263ca6 100644 --- a/EllesmereUIOptions/EllesmereUIOptions.toc +++ b/EllesmereUIOptions/EllesmereUIOptions.toc @@ -36,6 +36,7 @@ EUI_QoL_AutoLogging_Options.lua EUI_QoL_Shifter_Options.lua EUI_QoL_MovementAlert_Options.lua EUI_QoL_RaidTools_Options.lua +EUI_QoL_Swing_Options.lua EUI_UpgradeCalc_Options.lua EUI_QuestTracker_Options.lua EUI_Quickdraw_Options.lua diff --git a/EllesmereUIQoL/EllesmereUIQoL.lua b/EllesmereUIQoL/EllesmereUIQoL.lua index 4ab318928..806195273 100644 --- a/EllesmereUIQoL/EllesmereUIQoL.lua +++ b/EllesmereUIQoL/EllesmereUIQoL.lua @@ -48,6 +48,7 @@ local qolFrame = CreateFrame("Frame") qolFrame:RegisterEvent("PLAYER_LOGIN") qolFrame:SetScript("OnEvent", function(self) self:UnregisterEvent("PLAYER_LOGIN") + if _G._EUI_Swing_Apply then _G._EUI_Swing_Apply() end --------------------------------------------------------------------------- -- Auto Unwrap Collections (Mounts / Pets / Toys) diff --git a/EllesmereUIQoL/EllesmereUIQoL.toc b/EllesmereUIQoL/EllesmereUIQoL.toc index f7d5cbfef..7b1e5991c 100644 --- a/EllesmereUIQoL/EllesmereUIQoL.toc +++ b/EllesmereUIQoL/EllesmereUIQoL.toc @@ -20,6 +20,7 @@ EllesmereUIQoL_TeleportPrompt.lua EllesmereUIQoL_Shifter.lua EllesmereUIQoL_MovementAlert.lua EllesmereUIQoL_RaidTools.lua +EllesmereUIQoL_Swing.lua EUI_UpgradeCalc.lua # Options diff --git a/EllesmereUIQoL/EllesmereUIQoL_MovementAlert.lua b/EllesmereUIQoL/EllesmereUIQoL_MovementAlert.lua index a306cd145..ecbdc2745 100644 --- a/EllesmereUIQoL/EllesmereUIQoL_MovementAlert.lua +++ b/EllesmereUIQoL/EllesmereUIQoL_MovementAlert.lua @@ -791,9 +791,9 @@ local function SafeGetBaseDuration(spellId) end local function ResolvePlayerSpecId() - local spec = GetSpecialization() + local spec = C_SpecializationInfo.GetSpecialization() if not spec then return nil end - local specId = select(1, GetSpecializationInfo(spec)) + local specId = C_SpecializationInfo.GetSpecializationInfo(spec) if specId and specId > 0 then return specId end return nil end diff --git a/EllesmereUIQoL/EllesmereUIQoL_Swing.lua b/EllesmereUIQoL/EllesmereUIQoL_Swing.lua new file mode 100644 index 000000000..213f8cf67 --- /dev/null +++ b/EllesmereUIQoL/EllesmereUIQoL_Swing.lua @@ -0,0 +1,243 @@ +if EUI_CLIENT_BLOCKED then return end +local EUI = EllesmereUI +-- The player swing event and its enum are supplied by the Forever client. +if not (C_SwingTimer and Enum.PlayerSwingType and C_DurationUtil + and C_DurationUtil.CreateDurationTextBinding and C_StringUtil) then return end + +local defaults = { profile = { swingTimer = { + enabled = false, mainHand = true, offHand = true, ranged = true, + combatOnly = true, queueColor = true, width = 240, height = 18, + gap = 4, fontSize = 11, x = 0, y = -220, texture = "none", +} } } +local db, frame, registered, unlocked +local rows = {} +local labels = { "Main Hand", "Off Hand", "Ranged" } +local keys = { "mainHand", "offHand", "ranged" } +local types = { Enum.PlayerSwingType.MainHand, Enum.PlayerSwingType.OffHand, + Enum.PlayerSwingType.Ranged } +-- Base spell IDs resolve to localized spell names, including ranked casts. +local queuedSpells = { 78, 845, 6807 } +local textures + +local function Plain(value) + return not (issecretvalue and issecretvalue(value)) +end + +local function Profile() + if not db then db = EUI.Lite.NewDB("EllesmereUIQoLDB", defaults, true) end + return db.profile.swingTimer +end + +local function Position() + local p = Profile() + frame:ClearAllPoints() + frame:SetPoint("CENTER", UIParent, "CENTER", p.x, p.y) +end + +local function SetDuration(row, duration) + row.duration:SetTimeFromStart(GetTime(), duration) + row:SetTimerDuration(row.duration, Enum.StatusBarInterpolation.Immediate, + Enum.StatusBarTimerDirection.RemainingTime) + row.binding:SetDuration(row.duration) +end + +local function Reset() + for i = 1, #rows do + local row = rows[i] + SetDuration(row, 0) + row.label:SetText(labels[i]) + end +end + +local function PaintQueue() + if not frame then return end + local p = Profile() + local queuedName + if p.queueColor and C_Spell and C_Spell.IsCurrentSpell then + for i = 1, #queuedSpells do + local name = C_Spell.GetSpellName(queuedSpells[i]) + if Plain(name) and name then + local current = C_Spell.IsCurrentSpell(name) + if Plain(current) and current then queuedName = name; break end + end + end + end + local accent = EUI.ELLESMERE_GREEN + for i = 1, #rows do + local row = rows[i] + if queuedName and i < 3 then + row:SetStatusBarColor(1, 0.70, 0.20, 1) + row.label:SetText(labels[i] .. " - " .. queuedName) + else + row:SetStatusBarColor(accent.r, accent.g, accent.b, 1) + row.label:SetText(labels[i]) + end + end +end + +local function Layout() + if not frame then return end + local p = Profile() + local count = 0 + -- This is a player-visibility decision, not a secure-frame mutation gate. + -- Match Forever's own Swing Timer, which uses the unit combat predicate. + local visible = p.enabled and (not p.combatOnly or UnitAffectingCombat("player") or unlocked) + local font = EUI.GetFontPath("qol") + local main, off, ranged = UnitAttackSpeed("player") + local speeds = { main, off, ranged } + for i = 1, #rows do + local row = rows[i] + local speed = speeds[i] + if Plain(speed) then row.available = type(speed) == "number" and speed > 0 end + row:ClearAllPoints() + row:SetSize(p.width, p.height) + row:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, -count * (p.height + p.gap)) + row.label:SetFont(font, p.fontSize, "OUTLINE") + row.text:SetFont(font, p.fontSize, "OUTLINE") + row.label:SetWidth(math.max(1, p.width - 55)) + row:SetStatusBarTexture(EUI.ResolveTexturePath(textures, p.texture, "Interface\\Buttons\\WHITE8x8")) + local show = p[keys[i]] and row.available + row:SetShown(show) + if show then + count = count + 1 + if visible then row.binding:Enable() else row.binding:Disable() end + else + row.binding:Disable() + end + end + frame:SetSize(p.width, math.max(p.height, count * (p.height + p.gap) - p.gap)) + frame:SetShown(visible and count > 0) + PaintQueue() +end + +local function OnEvent(_, event, duration, swingType) + if event == "PLAYER_SWING" then + -- Never infer an interval from restricted data or another event's timing. + if not Plain(duration) or not Plain(swingType) then return end + if type(duration) ~= "number" or duration ~= duration or duration <= 0 + or duration == math.huge then return end + for i = 1, #rows do + if swingType == types[i] then + SetDuration(rows[i], duration) + break + end + end + PaintQueue() + elseif event == "ACTIONBAR_UPDATE_STATE" then + PaintQueue() + else + if event == "PLAYER_ENTERING_WORLD" or event == "PLAYER_DEAD" + or event == "WEAPON_SLOT_CHANGED" then Reset() end + Layout() + end +end + +local function CreateBars() + if frame then return end + frame = CreateFrame("Frame", "EUISwingTimer", UIParent) + frame:Hide() + frame:SetScript("OnEvent", OnEvent) + textures = EUI.BuildBarTextureTables() + local formatter = C_StringUtil.CreateSecondsFormatter() + for i = 1, #keys do + local row = CreateFrame("StatusBar", nil, frame) + row:SetStatusBarTexture("Interface\\Buttons\\WHITE8x8") + row:SetMinMaxValues(0, 1) + local bg = row:CreateTexture(nil, "BACKGROUND") + bg:SetAllPoints() + bg:SetColorTexture(0.035, 0.045, 0.055, 0.95) + EUI.MakeBorder(row, 0, 0, 0, 1) + row.label = row:CreateFontString(nil, "OVERLAY") + row.label:SetPoint("LEFT", row, "LEFT", 5, 0) + row.label:SetJustifyH("LEFT") + row.text = row:CreateFontString(nil, "OVERLAY") + row.text:SetPoint("RIGHT", row, "RIGHT", -5, 0) + row.duration = C_DurationUtil.CreateDuration() + row.duration:SetTimeFromStart(GetTime(), 0) + row:SetTimerDuration(row.duration, Enum.StatusBarInterpolation.Immediate, + Enum.StatusBarTimerDirection.RemainingTime) + row.binding = C_DurationUtil.CreateDurationTextBinding() + row.binding:SetFontString(row.text) + row.binding:SetDuration(row.duration) + row.binding:SetFormatter(formatter) + row.binding:SetExpiredText("Ready") + row.binding:SetZeroDurationText("Ready") + row.binding:SetUpdateInterval(0.1) + -- A player always has a main-hand swing. Off-hand and ranged remain + -- hidden until a readable attack-speed update confirms they are usable. + row.available = i == 1 + rows[i] = row + end +end + +local function Apply() + local p = Profile() + if not p.enabled then + if frame then + frame:UnregisterAllEvents() + if EUI.UnregisterUnlockModeListener then EUI:UnregisterUnlockModeListener(frame) end + unlocked = false + Reset() + frame:Hide() + for i = 1, #rows do rows[i].binding:Disable() end + end + return + end + CreateBars() + if EUI.RegisterUnlockModeListener then + EUI:RegisterUnlockModeListener(frame, function(active) + unlocked = active + Layout() + end) + end + frame:RegisterEvent("PLAYER_SWING") + frame:RegisterEvent("ACTIONBAR_UPDATE_STATE") + frame:RegisterEvent("PLAYER_REGEN_DISABLED") + frame:RegisterEvent("PLAYER_REGEN_ENABLED") + frame:RegisterEvent("PLAYER_ENTERING_WORLD") + frame:RegisterEvent("PLAYER_DEAD") + frame:RegisterEvent("WEAPON_SLOT_CHANGED") + frame:RegisterUnitEvent("UNIT_ATTACK_SPEED", "player") + Position() + Layout() + if not registered then + registered = true + EUI:RegisterUnlockElements({ EUI.MakeUnlockElement({ + key = "EUI_SwingTimer", label = "Swing Timer", group = "Quality of Life", order = 650, + -- Size comes from the page sliders, but Unlock Mode may still match + -- either dimension to another element through these same settings. + noResize = true, allowMatchSource = true, noAnchorTarget = true, + isHidden = function() return not Profile().enabled end, + getFrame = function() return frame end, + getSize = function() return frame:GetWidth(), frame:GetHeight() end, + setWidth = function(_, width) + local p = Profile() + local PP = EUI.PP + width = PP and PP.Snap and PP.Snap(width) or math.floor(width + 0.5) + p.width = math.max(100, math.min(600, width)) + Layout() + end, + setHeight = function(_, height) + local p = Profile() + p.height = math.max(12, math.min(40, math.floor(height + 0.5))) + Layout() + end, + savePos = function(_, _, _, x, y) + local s = Profile() + local cx, cy = frame:GetCenter() + local ux, uy = UIParent:GetCenter() + if cx and ux then s.x = cx - ux; s.y = cy - uy + else s.x = x; s.y = y end + end, + loadPos = function() + local s = Profile() + return { point = "CENTER", relPoint = "CENTER", x = s.x, y = s.y } + end, + clearPos = function() local s = Profile(); s.x = 0; s.y = -220 end, + applyPos = Position, + }) }) + end +end + +_G._EUI_Swing_Profile = Profile +_G._EUI_Swing_Apply = Apply diff --git a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua index 7f6fabde0..610228596 100644 --- a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua +++ b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua @@ -1,4 +1,5 @@ if EUI_CLIENT_BLOCKED then return end -- pre-12.1 client failsafe (EllesmereUI_ClientGate.lua) +local GetSpecialization = (C_SpecializationInfo and C_SpecializationInfo.GetSpecialization) or GetSpecialization local addonName, ns = ... if not (EllesmereUI and EllesmereUI._ModuleNS) then EUI_CLIENT_BLOCKED = true; return end -- stale-parent guard: a partially updated install (old parent, new child) goes dormant via the line-1 failsafe instead of erroring EllesmereUI._ModuleNS[addonName] = ns -- LOD options files read this module ns via the registry diff --git a/EllesmereUI_Profiles.lua b/EllesmereUI_Profiles.lua index da670e1f6..c8cf96bd9 100644 --- a/EllesmereUI_Profiles.lua +++ b/EllesmereUI_Profiles.lua @@ -1419,6 +1419,7 @@ local REFRESH_ADDON_STEPS = { if _G._EMIN_RefreshFlyout then _G._EMIN_RefreshFlyout() end end, -- Global class/power colors (updates oUF, nameplates, raid frames) + function() if _G._EUI_Swing_Apply then _G._EUI_Swing_Apply() end end, function() if EllesmereUI.ApplyColorsToOUF then EllesmereUI.ApplyColorsToOUF() end end, -- Re-register unlock elements for all modules whose bar sets can -- differ between profiles. Without this, _applySavedPositions uses