From 6e9b3250db0365ade9ccf9f057a2cbe46c4edfd0 Mon Sep 17 00:00:00 2001 From: Trenchy <128204689+Trenchfoote@users.noreply.github.com> Date: Sun, 7 Jun 2026 00:34:19 -0500 Subject: [PATCH] CDM: track engineering tinkers (Nitro Boosts) + Engineer Tinkers picker Adds support for tracking engineering belt tinkers on custom cooldown bars, starting with Nitro Boosts, and a dedicated "Engineer Tinkers" category in the add-spell picker (after Trinket Slots). - New enchant-preset mechanism (CDM_ENCHANT_PRESETS / ENCHANT_BY_SID), cloned from the trinket approach: cooldown comes from the equipped slot via GetInventoryItemCooldown, and the icon only injects when the tinker is actually applied (EnchantPresent). Presence requires both a usable on-use cooldown on the slot AND the equipped item's tooltip naming the tinker spell, so a belt with some other on-use can't false-positive; it falls back to the on-use check alone when tooltip/name data isn't ready. Cooldowns tick alongside trinkets; a belt re-enchant (PLAYER_EQUIPMENT_CHANGED on the slot) re-evaluates. - Options: "Engineer Tinkers" flyout submenu in the picker, listing the enchant presets (Nitro Boosts). Co-Authored-By: Claude Opus 4.8 --- .../EUI_CooldownManager_Options.lua | 153 ++++++++++++++++++ .../EllesmereUICdmHooks.lua | 118 ++++++++++++++ .../EllesmereUICooldownManager.lua | 22 +++ 3 files changed, 293 insertions(+) diff --git a/EllesmereUICooldownManager/EUI_CooldownManager_Options.lua b/EllesmereUICooldownManager/EUI_CooldownManager_Options.lua index acc17ee5..0d3f5d34 100644 --- a/EllesmereUICooldownManager/EUI_CooldownManager_Options.lua +++ b/EllesmereUICooldownManager/EUI_CooldownManager_Options.lua @@ -5708,6 +5708,158 @@ initFrame:SetScript("OnEvent", function(self) mH = mH + ITEM_H end + -- "Engineer Tinkers" flyout subnav (after Trinket Slots) + local _engSub + menu._engSub = nil -- reference for OnUpdate close-check + local enchPresets = ns.CDM_ENCHANT_PRESETS + if enchPresets and #enchPresets > 0 then + local engItem = CreateFrame("Button", nil, inner) + engItem:SetHeight(ITEM_H) + engItem:SetPoint("TOPLEFT", inner, "TOPLEFT", 1, -mH) + engItem:SetPoint("TOPRIGHT", inner, "TOPRIGHT", -1, -mH) + engItem:SetFrameLevel(menu:GetFrameLevel() + 2) + + local engHl = engItem:CreateTexture(nil, "ARTWORK") + engHl:SetAllPoints(); engHl:SetColorTexture(1, 1, 1, 0); engHl:SetAlpha(0) + + local engLbl = engItem:CreateFontString(nil, "OVERLAY") + engLbl:SetFont(FONT_PATH, 11, GetCDMOptOutline()) + engLbl:SetPoint("LEFT", 10, 0) + engLbl:SetJustifyH("LEFT") + engLbl:SetText(EllesmereUI.L("Engineer Tinkers")) + engLbl:SetTextColor(tDimR, tDimG, tDimB, tDimA) + + local engArrow = engItem:CreateTexture(nil, "ARTWORK") + engArrow:SetSize(10, 10) + engArrow:SetPoint("RIGHT", engItem, "RIGHT", -8, 0) + engArrow:SetTexture("Interface\\AddOns\\EllesmereUI\\media\\icons\\right-arrow.png") + engArrow:SetAlpha(0.7) + + local function ShowEngSub() + if not _engSub then + _engSub = CreateFrame("Frame", nil, menu) + menu._engSub = _engSub + _engSub:SetFrameStrata("FULLSCREEN_DIALOG") + _engSub:SetFrameLevel(menu:GetFrameLevel() + 5) + _engSub:SetClampedToScreen(true) + _engSub:EnableMouse(true) + elseif _engSub:IsShown() then + return + else + for _, child in ipairs({_engSub:GetChildren()}) do + child:Hide(); child:SetParent(nil) + end + for _, rgn in ipairs({_engSub:GetRegions()}) do + if rgn.Hide then rgn:Hide() end + end + end + + local subW = 220 + local SUB_ITEM_H = 26 + _engSub:SetSize(subW, 10) + _engSub:ClearAllPoints() + _engSub:SetPoint("TOPLEFT", engItem, "TOPRIGHT", 2, 0) + + local subBg = _engSub:CreateTexture(nil, "BACKGROUND") + subBg:SetAllPoints() + subBg:SetColorTexture(mBgR, mBgG, mBgB, mBgA) + EllesmereUI.MakeBorder(_engSub, 1, 1, 1, mBrdA, EllesmereUI.PP) + + local subInner = CreateFrame("Frame", nil, _engSub) + subInner:SetWidth(subW) + subInner:SetPoint("TOPLEFT") + + local subH = 4 + for _, preset in ipairs(enchPresets) do + local pID = preset.presetSID + local isAdded = alreadyOnBar[pID] + local pOtherBar = not isAdded and usedOnOtherBar[pID] + local pIsDisabled = isAdded or pOtherBar + + local si = CreateFrame("Button", nil, subInner) + si:SetHeight(SUB_ITEM_H) + si:SetPoint("TOPLEFT", subInner, "TOPLEFT", 1, -subH) + si:SetPoint("TOPRIGHT", subInner, "TOPRIGHT", -1, -subH) + si:SetFrameLevel(_engSub:GetFrameLevel() + 2) + si:RegisterForClicks("AnyUp") + + local sIco = si:CreateTexture(nil, "ARTWORK") + local icoSz = SUB_ITEM_H - 2 + sIco:SetSize(icoSz, icoSz) + sIco:SetPoint("RIGHT", si, "RIGHT", -6, 0) + sIco:SetTexture(preset.icon or (preset.spellID and C_Spell.GetSpellTexture(preset.spellID))) + sIco:SetTexCoord(0.08, 0.92, 0.08, 0.92) + + local sLbl = si:CreateFontString(nil, "OVERLAY") + sLbl:SetFont(FONT_PATH, 11, GetCDMOptOutline()) + sLbl:SetPoint("LEFT", si, "LEFT", 10, 0) + sLbl:SetPoint("RIGHT", sIco, "LEFT", -5, 0) + sLbl:SetJustifyH("LEFT") + sLbl:SetWordWrap(false) + sLbl:SetMaxLines(1) + sLbl:SetText(preset.name) + + local sHl = si:CreateTexture(nil, "ARTWORK") + sHl:SetAllPoints() + sHl:SetColorTexture(1, 1, 1, 0); sHl:SetAlpha(0) + + if pIsDisabled then + sLbl:SetTextColor(tDimR, tDimG, tDimB, tDimA * 0.4) + sIco:SetDesaturated(true) + sIco:SetAlpha(0.4) + local pTooltipName = isAdded and (bd and (bd.name or bd.key) or barKey) or pOtherBar + si:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(si, "Already on " .. pTooltipName) + end) + si:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + else + sLbl:SetTextColor(tDimR, tDimG, tDimB, tDimA) + si:SetScript("OnEnter", function() + sLbl:SetTextColor(1, 1, 1, 1) + sHl:SetColorTexture(1, 1, 1, hlA); sHl:SetAlpha(1) + end) + si:SetScript("OnLeave", function() + sLbl:SetTextColor(tDimR, tDimG, tDimB, tDimA) + sHl:SetAlpha(0) + end) + si:SetScript("OnClick", function() + _engSub:Hide() + menu:Hide() + EnsureAssignedSpells(barKey) + ns.AddTrackedSpell(barKey, pID) + RefreshCDPreview() + end) + end + subH = subH + SUB_ITEM_H + end + + local totalSubH = subH + 4 + subInner:SetHeight(totalSubH) + _engSub:SetHeight(totalSubH) + subInner:SetParent(_engSub) + subInner:SetPoint("TOPLEFT") + _engSub:Show() + end + + engItem:SetScript("OnEnter", function() + engLbl:SetTextColor(1, 1, 1, 1) + engHl:SetColorTexture(1, 1, 1, hlA); engHl:SetAlpha(1) + ShowEngSub() + end) + engItem:SetScript("OnLeave", function() + engLbl:SetTextColor(tDimR, tDimG, tDimB, tDimA) + engHl:SetAlpha(0) + C_Timer.After(0.3, function() + if _engSub and _engSub:IsShown() and not _engSub:IsMouseOver() and not engItem:IsMouseOver() then + _engSub:Hide() + end + end) + end) + + allItems[#allItems + 1] = engItem + mH = mH + ITEM_H + end + -- Racial abilities local _pRace = ns._playerRace local _pClass = ns._playerClass @@ -6205,6 +6357,7 @@ initFrame:SetScript("OnEvent", function(self) menu:SetScript("OnUpdate", function(m) local overSub = (_customTrackingSub and _customTrackingSub:IsShown() and _customTrackingSub:IsMouseOver()) or (m._potionsSub and m._potionsSub:IsShown() and m._potionsSub:IsMouseOver()) + or (m._engSub and m._engSub:IsShown() and m._engSub:IsMouseOver()) if not m:IsMouseOver() and not anchorFrame:IsMouseOver() and not overSub and IsMouseButtonDown("LeftButton") then m:Hide() end diff --git a/EllesmereUICooldownManager/EllesmereUICdmHooks.lua b/EllesmereUICooldownManager/EllesmereUICdmHooks.lua index 014bb1f4..71aedd14 100644 --- a/EllesmereUICooldownManager/EllesmereUICdmHooks.lua +++ b/EllesmereUICooldownManager/EllesmereUICdmHooks.lua @@ -1075,6 +1075,101 @@ local function UpdateTrinketCooldown(slotID) end end +------------------------------------------------------------------------------- +-- Enchant presets (engineering tinkers, e.g. Nitro Boosts on the belt). +-- Cloned from the trinket mechanism: cooldown comes from the equipped slot via +-- GetInventoryItemCooldown, NOT a bag item. Presence is detected by the slot +-- having a usable on-use effect (enable == 1). +------------------------------------------------------------------------------- +local _enchantFrames = {} -- [presetSID] = frame + +-- The slot has the tinker if GetInventoryItemCooldown reports a usable on-use +-- (enable == 1) AND the equipped item's tooltip names the tinker spell -- a belt +-- with some *other* on-use effect would otherwise false-positive. Matching the +-- localized spell name against tooltip text keeps it locale-independent (both +-- come from the client). Falls back to the on-use check alone when the name or +-- tooltip data isn't available yet, so we never over-hide on missing data. +local function EnchantPresent(slot, spellID) + local _, _, enable = GetInventoryItemCooldown("player", slot) + if enable ~= 1 then return false end + local name = spellID and C_Spell and C_Spell.GetSpellName and C_Spell.GetSpellName(spellID) + if not name then return true end + local data = C_TooltipInfo and C_TooltipInfo.GetInventoryItem + and C_TooltipInfo.GetInventoryItem("player", slot) + if not data or not data.lines then return true end + for _, line in ipairs(data.lines) do + local lt = line.leftText + if lt and lt:find(name, 1, true) then return true end + end + return false +end + +local function GetOrCreateEnchantFrame(entry) + local sid = entry.presetSID + local f = _enchantFrames[sid] + if f then return f end + + f = CreateFrame("Frame", nil, UIParent) + f:SetSize(36, 36) + f:Hide() + f:EnableMouse(false) + + local tex = f:CreateTexture(nil, "ARTWORK") + tex:SetAllPoints() + tex:SetTexCoord(0.08, 0.92, 0.08, 0.92) + if entry.icon then tex:SetTexture(entry.icon) end + f.Icon = tex + f._tex = tex + + local cd = CreateFrame("Cooldown", nil, f, "CooldownFrameTemplate") + cd:SetAllPoints() + cd:SetDrawEdge(false) + cd:SetDrawBling(false) + cd:SetHideCountdownNumbers(true) + cd:EnableMouse(false) + if cd.SetMouseClickEnabled then cd:SetMouseClickEnabled(false) end + if cd.SetMouseMotionEnabled then cd:SetMouseMotionEnabled(false) end + f.Cooldown = cd + f._cooldown = cd + + f._isEnchantFrame = true + f._enchantSlot = entry.slot + f._enchantSpellID = entry.spellID + f._enchantSID = sid + f.cooldownID = nil + f.cooldownInfo = nil + f.layoutIndex = 99992 + f.cooldownDuration = 0 + + f:EnableMouse(true) + if f.SetMouseClickEnabled then f:SetMouseClickEnabled(false) end + f:SetScript("OnEnter", function(self) + local ffc = _ecmeFC[self] + local bd2 = ffc and ffc.barKey and barDataByKey[ffc.barKey] + if not bd2 or not bd2.showTooltip then return end + GameTooltip_SetDefaultAnchor(GameTooltip, self) + GameTooltip:SetInventoryItem("player", self._enchantSlot) + end) + f:SetScript("OnLeave", GameTooltip_Hide) + + _enchantFrames[sid] = f + return f +end + +local function UpdateEnchantCooldown(f) + if not f then return false end + local start, dur, enable = GetInventoryItemCooldown("player", f._enchantSlot) + if start and dur and dur > 1.5 and enable == 1 then + f._cooldown:SetCooldown(start, dur) + if f._tex then f._tex:SetDesaturated(true) end + return true + else + f._cooldown:Clear() + if f._tex then f._tex:SetDesaturated(false) end + return false + end +end + local _trinketEventFrame = CreateFrame("Frame") _trinketEventFrame:RegisterEvent("PLAYER_EQUIPMENT_CHANGED") _trinketEventFrame:RegisterEvent("SPELL_UPDATE_COOLDOWN") @@ -1093,6 +1188,11 @@ _trinketEventFrame:SetScript("OnEvent", function(_, event, arg1) end) end end + -- Enchant slot changed (e.g. belt re-enchant): a tinker may have been + -- added or removed, so re-evaluate presence via a rebuild. + for _, e in ipairs(ns.CDM_ENCHANT_PRESETS or {}) do + if e.slot == arg1 and ns.QueueReanchor then ns.QueueReanchor(); break end + end elseif event == "PLAYER_ENTERING_WORLD" then UpdateTrinketFrame(13) UpdateTrinketFrame(14) @@ -1119,6 +1219,10 @@ _trinketEventFrame:SetScript("OnEvent", function(_, event, arg1) UpdateTrinketCooldown(slot) end end + -- Enchant cooldowns (Nitro shares the potion CD) tick here too. + for _, f in pairs(_enchantFrames) do + if f:IsShown() then UpdateEnchantCooldown(f) end + end end end) @@ -1759,6 +1863,20 @@ local function CollectAndReanchor() else tf:Hide() end + elseif sid and ns.ENCHANT_BY_SID and ns.ENCHANT_BY_SID[sid] then + -- Enchant preset (engineering tinker, e.g. Nitro Boosts). + -- Only inject when the slot actually has the tinker, so the + -- icon never shows on a character/belt without it. + local entry = ns.ENCHANT_BY_SID[sid] + local ef = GetOrCreateEnchantFrame(entry) + if EnchantPresent(entry.slot, entry.spellID) then + UpdateEnchantCooldown(ef) + frames[#frames + 1] = ef + local fc = FC(ef) + fc.barKey = barKey; fc.spellID = sid + else + ef:Hide() + end elseif sid and sid <= -100 then -- Item preset (potions, healthstone, etc.) local itemID = -sid diff --git a/EllesmereUICooldownManager/EllesmereUICooldownManager.lua b/EllesmereUICooldownManager/EllesmereUICooldownManager.lua index bf8cc93a..49ce6e78 100644 --- a/EllesmereUICooldownManager/EllesmereUICooldownManager.lua +++ b/EllesmereUICooldownManager/EllesmereUICooldownManager.lua @@ -399,6 +399,28 @@ local CDM_ITEM_PRESETS = { } ns.CDM_ITEM_PRESETS = CDM_ITEM_PRESETS +-- Equipped-slot enchant presets (engineering tinkers). Tracked like trinkets via +-- GetInventoryItemCooldown on a fixed inventory slot -- NOT a bag item, so they +-- have no itemID/GetItemCount. presetSID is a stable negative handle in the +-- reserved enchant range (-50..-59) that can't collide with trinket (-13/-14) or +-- item (<= -100) handles. Icon resolved from the spell at load. +local CDM_ENCHANT_PRESETS = { + { + key = "nitro_boosts", + name = "Nitro Boosts", + spellID = 55016, + slot = INVSLOT_WAIST or 6, -- belt tinker + presetSID = -50, + icon = (C_Spell and C_Spell.GetSpellTexture and C_Spell.GetSpellTexture(55016)) or 136243, + }, +} +ns.CDM_ENCHANT_PRESETS = CDM_ENCHANT_PRESETS + +-- Lookup: presetSID -> entry (for fast injection-time resolution). +local ENCHANT_BY_SID = {} +for _, e in ipairs(CDM_ENCHANT_PRESETS) do ENCHANT_BY_SID[e.presetSID] = e end +ns.ENCHANT_BY_SID = ENCHANT_BY_SID + local BuildAllCDMBars local RegisterCDMUnlockElements