From 52ef5a37338d2903ada1eaadeb31a2ea6a7084a9 Mon Sep 17 00:00:00 2001 From: BanditTech Date: Sat, 8 Nov 2025 03:31:18 -0800 Subject: [PATCH] Deconstruct mode --- ElvUI/Modules/Bags/Bags.lua | 18 + ElvUI/Modules/Bags/Deconstruct.lua | 572 +++++++++++++++++++++++++++++ ElvUI/Modules/Bags/Load_Bags.xml | 1 + 3 files changed, 591 insertions(+) create mode 100644 ElvUI/Modules/Bags/Deconstruct.lua diff --git a/ElvUI/Modules/Bags/Bags.lua b/ElvUI/Modules/Bags/Bags.lua index 5e0d4d8..f719df5 100644 --- a/ElvUI/Modules/Bags/Bags.lua +++ b/ElvUI/Modules/Bags/Bags.lua @@ -1438,6 +1438,24 @@ function B:ContructContainerFrame(name, isBank) f.vendorGraysButton:SetScript("OnLeave", GameTooltip_Hide) f.vendorGraysButton:SetScript("OnClick", B.VendorGrayCheck) + -- Deconstruct button + f.deconstructButton = CreateFrame("Button", nil, f.holderFrame) + f.deconstructButton:Size(16 + E.Border) + f.deconstructButton:SetTemplate() + f.deconstructButton:Point("RIGHT", f.vendorGraysButton, "LEFT", -5, 0) + f.deconstructButton:SetNormalTexture("Interface\\ICONS\\INV_Rod_Enchantedcobalt") + f.deconstructButton:GetNormalTexture():SetTexCoord(unpack(E.TexCoords)) + f.deconstructButton:GetNormalTexture():SetInside() + f.deconstructButton:SetPushedTexture("Interface\\ICONS\\INV_Rod_Enchantedcobalt") + f.deconstructButton:GetPushedTexture():SetTexCoord(unpack(E.TexCoords)) + f.deconstructButton:GetPushedTexture():SetInside() + f.deconstructButton:StyleButton(nil, true) + f.deconstructButton.ttText = "Deconstruct Mode" + f.deconstructButton.ttText2 = "Allow you to disenchant/mill/prospect/unlock items.\nClick to toggle.\nCurrent state: |cffFF0000Disabled|r" + f.deconstructButton:SetScript("OnEnter", B.Tooltip_Show) + f.deconstructButton:SetScript("OnLeave", GameTooltip_Hide) + -- OnClick handler will be set by Deconstruct module + --Search f.editBox = CreateFrame("EditBox", name.."EditBox", f) f.editBox:SetFrameLevel(f.editBox:GetFrameLevel() + 2) diff --git a/ElvUI/Modules/Bags/Deconstruct.lua b/ElvUI/Modules/Bags/Deconstruct.lua new file mode 100644 index 0000000..10e306b --- /dev/null +++ b/ElvUI/Modules/Bags/Deconstruct.lua @@ -0,0 +1,572 @@ +--[[ + Deconstruct Module for ElvUI (WoW 3.3.5a / Ascension) + Adapted from ElvUI_SLE retail version + + This module provides functionality to disenchant, mill, prospect, and unlock items + directly from bags by creating an overlay button when mousing over compatible items. +]] -- +local E, L, V, P, G = unpack(select(2, ...)) +local B = E:GetModule("Bags") + +local D = B:NewModule("Deconstruct", "AceHook-3.0", "AceEvent-3.0") + +-- Lua functions +local _G = _G +local format, strfind, type, tostring = format, strfind, type, tostring +local pairs, select, unpack = pairs, select, unpack + +-- WoW API +local GetTradeTargetItemLink = GetTradeTargetItemLink +local InCombatLockdown = InCombatLockdown +local GetContainerItemLink = GetContainerItemLink +local GetSpellInfo = GetSpellInfo +local GetItemInfo = GetItemInfo +local GetItemCount = GetItemCount +local CreateFrame = CreateFrame +local GameTooltip = GameTooltip + +-- Constants (with fallbacks in case they're not loaded yet) +local LOCKED = LOCKED or "Locked" +local VIDEO_OPTIONS_ENABLED = VIDEO_OPTIONS_ENABLED or "Enabled" +local VIDEO_OPTIONS_DISABLED = VIDEO_OPTIONS_DISABLED or "Disabled" + +-- Module variables +D.DeconstructMode = false +D.ItemTable = { + ['DoNotDE'] = { + ['49715'] = true, -- Rose helm + ['44731'] = true, -- Rose offhand + ['21524'] = true, -- Red winter hat + ['51525'] = true, -- Green winter hat + ['70923'] = true, -- Sweater + ['34486'] = true, -- Orgrimmar achievement fish + ['11287'] = true, -- Lesser Magic Wand + ['11288'] = true -- Greater Magic Wand + }, + ['Cooking'] = { + ['46349'] = true -- Chef's Hat + }, + ['Fishing'] = { + ['19022'] = true, -- Nat Pagle's Extreme Angler FC-5000 + ['19970'] = true, -- Arcanite Fishing Pole + ['25978'] = true, -- Seth's Graphite Fishing Pole + ['44050'] = true, -- Mastercraft Kalu'ak Fishing Pole + ['45858'] = true, -- Nat's Lucky Fishing Pole + ['45991'] = true, -- Bone Fishing Pole + ['45992'] = true -- Jeweled Fishing Pole + } +} + +D.Keys = {} +D.BlacklistDE = {} +D.BlacklistLOCK = {} + +-- Profession spell names +D.DEname = GetSpellInfo(13262) -- Disenchant +D.MILLname = GetSpellInfo(51005) -- Milling +D.PROSPECTname = GetSpellInfo(31252) -- Prospecting +D.LOCKname = GetSpellInfo(1804) -- Pick Lock + +-- Check if player has any relevant professions +function D:HasRelevantProfession() + if D.HasEnchanting then return true end + if D.HasInscription then return true end + if D.HasJewelcrafting then return true end + if D.HasPickLock then return true end + return false +end + +-- Check which professions the player has +function D:UpdateProfessions() + D.HasEnchanting = false + D.HasInscription = false + D.HasJewelcrafting = false + D.HasPickLock = false + + if D.DEname and GetSpellInfo(D.DEname) then D.HasEnchanting = true end + if D.MILLname and GetSpellInfo(D.MILLname) then D.HasInscription = true end + if D.PROSPECTname and GetSpellInfo(D.PROSPECTname) then D.HasJewelcrafting = true end + if D.LOCKname and GetSpellInfo(D.LOCKname) then D.HasPickLock = true end +end + +-- Helper function to check if player has a skeleton key +local function HaveKey() for key in pairs(D.Keys) do if GetItemCount(key) > 0 then return key end end end + +-- Build blacklist from settings +function D:Blacklisting(skill) D['BuildBlacklist' .. skill](self) end + +function D:BuildBlacklistDE(...) + wipe(D.BlacklistDE) + for index = 1, select('#', ...) do + local name = select(index, ...) + if name and name ~= "" then + local itemName = GetItemInfo(name) + if itemName then D.BlacklistDE[itemName] = true end + end + end +end + +function D:BuildBlacklistLOCK(...) + wipe(D.BlacklistLOCK) + for index = 1, select('#', ...) do + local name = select(index, ...) + if name and name ~= "" then + local itemName = GetItemInfo(name) + if itemName then D.BlacklistLOCK[itemName] = true end + end + end +end + +-- Check if item can be disenchanted +function D:IsBreakable(itemId, itemName, itemQuality, equipSlot) + if not itemId then return false end + if type(itemId) == "number" then itemId = tostring(itemId) end + + -- Check blacklists + if D.ItemTable['DoNotDE'][itemId] then return false end + if D.ItemTable['Cooking'][itemId] then return false end + if D.ItemTable['Fishing'][itemId] then return false end + if D.BlacklistDE[itemName] then return false end + + return true +end + +-- Check if item is disenchantable +function D:IsDisenchantable(itemId) + if not itemId then return false end + + local itemName, itemLink, itemRarity, itemLevel, itemMinLevel, itemType, itemSubType, itemStackCount, itemEquipLoc = GetItemInfo(itemId) + if not itemName then return false end + + -- Quality: 2=Uncommon, 3=Rare, 4=Epic + if not itemRarity or itemRarity < 2 or itemRarity > 4 then return false end + + if itemType ~= "Armor" and itemType ~= "Weapon" then return false end + if not itemEquipLoc or itemEquipLoc == "" then return false end + if not D.HasEnchanting then return false end + + return true +end + +-- Check if item can be prospected +function D:IsProspectable(itemId) + if not itemId or not D.HasJewelcrafting then return false end + + -- Prospectable ores in WotLK (3.3.5a) + local prospectableOres = { + [2770] = true, -- Copper Ore + [2771] = true, -- Tin Ore + [2772] = true, -- Iron Ore + [3858] = true, -- Mithril Ore + [10620] = true, -- Thorium Ore + [23424] = true, -- Fel Iron Ore + [23425] = true, -- Adamantite Ore + [36909] = true, -- Cobalt Ore + [36912] = true, -- Saronite Ore + [36910] = true -- Titanium Ore + } + + return prospectableOres[tonumber(itemId)] or false +end + +-- Check if item can be milled +function D:IsMillable(itemId) + if not itemId or not D.HasInscription then return false end + + -- Millable herbs in WotLK (3.3.5a) + local millableHerbs = { + [765] = true, -- Silverleaf + [2447] = true, -- Peacebloom + [2449] = true, -- Earthroot + [785] = true, -- Mageroyal + [2450] = true, -- Briarthorn + [2452] = true, -- Swiftthistle + [2453] = true, -- Bruiseweed + [3820] = true, -- Stranglekelp + [3369] = true, -- Grave Moss + [3356] = true, -- Kingsblood + [3357] = true, -- Liferoot + [3818] = true, -- Fadeleaf + [3821] = true, -- Goldthorn + [3358] = true, -- Khadgar's Whisker + [3819] = true, -- Dragon's Teeth (Wintersbite) + [8836] = true, -- Arthas' Tears + [8838] = true, -- Sungrass + [8839] = true, -- Blindweed + [8845] = true, -- Ghost Mushroom + [8846] = true, -- Gromsblood + [13464] = true, -- Golden Sansam + [13463] = true, -- Dreamfoil + [13465] = true, -- Mountain Silversage + [13466] = true, -- Plaguebloom + [13467] = true, -- Icecap + [22785] = true, -- Felweed + [22786] = true, -- Dreaming Glory + [22787] = true, -- Ragveil + [22789] = true, -- Terocone + [22790] = true, -- Ancient Lichen + [22791] = true, -- Netherbloom + [22792] = true, -- Nightmare Vine + [22793] = true, -- Mana Thistle + [36901] = true, -- Goldclover + [36903] = true, -- Adder's Tongue + [36904] = true, -- Tiger Lily + [36905] = true, -- Lichbloom + [36906] = true, -- Icethorn + [36907] = true, -- Talandra's Rose + [37921] = true, -- Deadnettle + [39970] = true -- Fire Leaf + } + + return millableHerbs[tonumber(itemId)] or false +end + +-- Check if item is currently locked (scan tooltip) +function D:IsUnlockable(itemLink) + if not itemLink then return false end + + -- Scan tooltip for "Locked" text + GameTooltip:SetOwner(UIParent, "ANCHOR_NONE") + GameTooltip:SetHyperlink(itemLink) + + for i = 2, GameTooltip:NumLines() do + local line = _G["GameTooltipTextLeft" .. i] + if line then + local text = line:GetText() + if text and strfind(text, LOCKED) then + GameTooltip:Hide() + return true + end + end + end + + GameTooltip:Hide() + return false +end + +-- Apply the deconstruct overlay to a bag slot +function D:ApplyDeconstruct(itemLink, itemId, spell, spellType, r, g, b, slot) + if not slot then return end + if slot == D.DeconstructionReal then return end + + local bag = slot:GetParent():GetID() + + -- Verify the bag and slot exist in ElvUI's bag or bank structure + local validBag = (B.BagFrame and B.BagFrame.Bags and B.BagFrame.Bags[bag]) or (B.BankFrame and B.BankFrame.Bags and B.BankFrame.Bags[bag]) + if not validBag then return end + + D.DeconstructionReal.Bag = bag + D.DeconstructionReal.Slot = slot:GetID() + + -- Check if in trade window + if GetTradeTargetItemLink and GetTradeTargetItemLink(7) == itemLink then + return + elseif GetContainerItemLink(bag, slot:GetID()) == itemLink then + D.DeconstructionReal.ID = itemId + D.DeconstructionReal:SetAttribute('type1', spellType) + D.DeconstructionReal:SetAttribute(spellType, spell) + D.DeconstructionReal:SetAttribute('target-bag', D.DeconstructionReal.Bag) + D.DeconstructionReal:SetAttribute('target-slot', D.DeconstructionReal.Slot) + D.DeconstructionReal:SetAllPoints(slot) + D.DeconstructionReal:Show() + + -- Apply visual effect (ActionButton_ShowOverlayGlow confirmed on Ascension) + ActionButton_ShowOverlayGlow(D.DeconstructionReal) + end +end + +-- Get deconstruct mode status text +function D:GetDeconMode() + local text + if D.DeconstructMode then + text = '|cff00FF00 ' .. VIDEO_OPTIONS_ENABLED .. '|r' + else + text = '|cffFF0000 ' .. VIDEO_OPTIONS_DISABLED .. '|r' + end + return text +end + +-- Toggle deconstruct mode (called from button click) +function D:ToggleMode() + D.DeconstructMode = not D.DeconstructMode + + if D.DeconstructButton then + local normalTex = D.DeconstructButton:GetNormalTexture() + if normalTex then + if D.DeconstructMode then + normalTex:SetTexture([[Interface\ICONS\INV_Enchant_EssenceCosmicGreater]]) + ActionButton_ShowOverlayGlow(D.DeconstructButton) + else + normalTex:SetTexture([[Interface\ICONS\INV_Rod_Enchantedcobalt]]) + ActionButton_HideOverlayGlow(D.DeconstructButton) + end + end + + D.DeconstructButton.ttText2 = format("Allow you to disenchant/mill/prospect/unlock items.\nClick to toggle.\nCurrent state: %s.", D:GetDeconMode()) + if GameTooltip:IsOwned(D.DeconstructButton) then B.Tooltip_Show(D.DeconstructButton) end + end + + -- Update both bag and bank slots + if B.BagFrame then D:UpdateBagSlots(B.BagFrame, D.DeconstructMode) end + if B.BankFrame then D:UpdateBagSlots(B.BankFrame, D.DeconstructMode) end +end + +-- Create the actual deconstruct button overlay +function D:ConstructRealDecButton() + D.DeconstructionReal = CreateFrame('Button', 'ElvUI_DeconReal', E.UIParent, 'SecureActionButtonTemplate') + D.DeconstructionReal:SetScript('OnEvent', function(obj, event, ...) obj[event](obj, ...) end) + D.DeconstructionReal:RegisterForClicks('AnyUp', 'AnyDown') + D.DeconstructionReal:SetFrameStrata('TOOLTIP') + + D.DeconstructionReal.OnLeave = function(frame) + if InCombatLockdown() then + frame:SetAlpha(0) + frame:RegisterEvent('PLAYER_REGEN_ENABLED') + else + frame:ClearAllPoints() + frame:SetAlpha(1) + if GameTooltip then GameTooltip:Hide() end + + -- Stop visual effects + ActionButton_HideOverlayGlow(frame) + + frame:Hide() + end + end + + D.DeconstructionReal.SetTip = function(f) + GameTooltip:SetOwner(f, 'ANCHOR_LEFT', 0, 4) + GameTooltip:ClearLines() + GameTooltip:SetBagItem(f.Bag, f.Slot) + end + + D.DeconstructionReal:SetScript('OnEnter', D.DeconstructionReal.SetTip) + D.DeconstructionReal:SetScript('OnLeave', function() D.DeconstructionReal:OnLeave() end) + D.DeconstructionReal:Hide() + + function D.DeconstructionReal:PLAYER_REGEN_ENABLED() + self:UnregisterEvent('PLAYER_REGEN_ENABLED') + D.DeconstructionReal:OnLeave() + end +end + +-- Parse tooltip to determine if item should show deconstruct button +function D:DeconstructParser() + if not D.DeconstructMode then return end + if not GameTooltip:IsVisible() then return end + + local owner = GameTooltip:GetOwner() + if not owner then return end + + local ownerName = owner.GetName and owner:GetName() + if not ownerName then return end + + -- Only process ElvUI bag and bank slots + if not (strfind(ownerName, 'ElvUI_ContainerFrameBag') or strfind(ownerName, 'ElvUI_BankContainerFrameBag')) then return end + + -- Get the bag and slot from the owner + local bag, slot + if owner.GetParent then + local parent = owner:GetParent() + if parent.GetID then bag = parent:GetID() end + end + if owner.GetID then slot = owner:GetID() end + + if not bag or not slot then return end + + local itemLink = GetContainerItemLink(bag, slot) + if not itemLink then return end + + local itemId = tonumber(itemLink:match("item:(%d+)")) + if not itemId then return end + + if InCombatLockdown() then return end + + -- Check what can be done with this item + local r, g, b + + -- Check for lockboxes + local hasKey = HaveKey() + if (D.HasPickLock or hasKey) and D:IsUnlockable(itemLink) then + r, g, b = 0, 1, 1 + if D.HasPickLock then + D:ApplyDeconstruct(itemLink, itemId, D.LOCKname, 'spell', r, g, b, owner) + elseif hasKey then + D:ApplyDeconstruct(itemLink, itemId, hasKey, 'item', r, g, b, owner) + end + return + end + + -- Check for prospectable ores (Jewelcrafting only) + if D.HasJewelcrafting and D:IsProspectable(itemId) then + r, g, b = 1, 0.5, 0 + D:ApplyDeconstruct(itemLink, itemId, D.PROSPECTname, 'spell', r, g, b, owner) + return + end + + -- Check for millable herbs (Inscription only) + if D.HasInscription and D:IsMillable(itemId) then + r, g, b = 0, 1, 0 + D:ApplyDeconstruct(itemLink, itemId, D.MILLname, 'spell', r, g, b, owner) + return + end + + -- Check for disenchantable items (Enchanting only) + if D.HasEnchanting and D:IsDisenchantable(itemId) then + local itemName, _, itemQuality, _, _, _, _, _, equipSlot = GetItemInfo(itemId) + if D:IsBreakable(itemId, itemName, itemQuality, equipSlot) then + r, g, b = 0.5, 0, 1 + D:ApplyDeconstruct(itemLink, itemId, D.DEname, 'spell', r, g, b, owner) + return + end + end +end + +-- Check if an item can be processed (disenchant/mill/prospect/unlock) +function D:CanProcessItem(itemLink, hasKey) + if not itemLink then return false end + + local itemId = tonumber(itemLink:match("item:(%d+)")) + if not itemId then return false end + + -- Check lockboxes (Rogues only) + if (D.HasPickLock or hasKey) and D:IsUnlockable(itemLink) then return true end + + -- Check prospectable (Jewelcrafting only) + if D.HasJewelcrafting and D:IsProspectable(itemId) then return true end + + -- Check millable (Inscription only) + if D.HasInscription and D:IsMillable(itemId) then return true end + + -- Check disenchantable (Enchanting only) + if D.HasEnchanting and D:IsDisenchantable(itemId) then + local itemName, _, itemQuality, _, _, _, _, _, equipSlot = GetItemInfo(itemId) + if D:IsBreakable(itemId, itemName, itemQuality, equipSlot) then return true end + end + + return false +end + +-- Update all bag/bank slots to dim non-processable items +function D:UpdateBagSlots(frame, isActive) + if not frame or not frame.Bags then return end + + local hasKey = HaveKey() + for _, bagID in ipairs(frame.BagIDs) do + if frame.Bags[bagID] then + for slotID = 1, GetContainerNumSlots(bagID) do + local slot = frame.Bags[bagID][slotID] + if slot then + if isActive then + -- Dim items that can't be processed + local itemLink = GetContainerItemLink(bagID, slotID) + if itemLink and D:CanProcessItem(itemLink, hasKey) then + slot:SetAlpha(1) + else + slot:SetAlpha(0.3) + end + else + -- Restore normal alpha + slot:SetAlpha(1) + end + end + end + end + end +end + +-- Helper function to setup button and hooks (called once when bags are first opened) +local function SetupDeconstructButton() + if D.DeconstructButton then return end + + -- Update professions first + D:UpdateProfessions() + + -- Only show button if player has relevant professions + if not D:HasRelevantProfession() then return end + + if not B.BagFrame or not B.BagFrame.deconstructButton then return end + + -- Store button reference + D.DeconstructButton = B.BagFrame.deconstructButton + D.DeconstructButton:SetScript("OnClick", function() D:ToggleMode() end) + + -- Only create the real deconstruct button and hooks once + if not D.DeconstructionReal then + D:ConstructRealDecButton() + + -- Hook GameTooltip to detect when mousing over items + GameTooltip:HookScript('OnShow', function() D:DeconstructParser() end) + GameTooltip:HookScript('OnUpdate', function() D:DeconstructParser() end) + end + + -- Hide deconstruct mode when bags close + B.BagFrame:HookScript('OnHide', function() + D.DeconstructMode = false + if D.DeconstructButton then + local normalTex = D.DeconstructButton:GetNormalTexture() + if normalTex then normalTex:SetTexture([[Interface\ICONS\INV_Rod_Enchantedcobalt]]) end + ActionButton_HideOverlayGlow(D.DeconstructButton) + end + if B.BagFrame then D:UpdateBagSlots(B.BagFrame, false) end + if B.BankFrame then D:UpdateBagSlots(B.BankFrame, false) end + if D.DeconstructionReal then D.DeconstructionReal:OnLeave() end + end) +end + +-- Initialize the module +function D:Initialize() + if not E.private.bags.enable then return end + + -- Hook into Layout to setup button for bags and reapply dimming + hooksecurefunc(B, "Layout", function(_, isBank) + if not isBank then if B.BagFrame and not D.DeconstructButton then E:Delay(0.1, function() SetupDeconstructButton() end) end end + + -- Reapply dimming after layout (layout resets slot alpha) + if D.DeconstructMode then + E:Delay(0.05, function() + if B.BagFrame then D:UpdateBagSlots(B.BagFrame, true) end + if B.BankFrame then D:UpdateBagSlots(B.BankFrame, true) end + end) + end + end) + + -- Build blacklists + D:Blacklisting('DE') + D:Blacklisting('LOCK') + + -- Register events + D:RegisterEvent('BAG_UPDATE') + D:RegisterEvent('SKILL_LINES_CHANGED') + + -- If bag frame already exists, setup button now + if B.BagFrame and not D.DeconstructButton then E:Delay(0.1, function() SetupDeconstructButton() end) end +end + +-- Handle SKILL_LINES_CHANGED event +function D:SKILL_LINES_CHANGED() + D:UpdateProfessions() + + -- Show/hide button based on professions + local hasProf = D:HasRelevantProfession() + + if D.DeconstructButton then + if hasProf then + D.DeconstructButton:Show() + else + D.DeconstructButton:Hide() + D.DeconstructMode = false + end + end +end + +-- Handle BAG_UPDATE event +function D:BAG_UPDATE() + if D.DeconstructMode then + if B.BagFrame then D:UpdateBagSlots(B.BagFrame, true) end + if B.BankFrame then D:UpdateBagSlots(B.BankFrame, true) end + end +end + +-- Wait for ElvUI to fully load before initializing +hooksecurefunc(B, "Initialize", function() D:Initialize() end) diff --git a/ElvUI/Modules/Bags/Load_Bags.xml b/ElvUI/Modules/Bags/Load_Bags.xml index 3213cc4..2b03112 100644 --- a/ElvUI/Modules/Bags/Load_Bags.xml +++ b/ElvUI/Modules/Bags/Load_Bags.xml @@ -2,4 +2,5 @@