local addonName = ... local SuffixQuery = CreateFrame("Frame") _G[addonName] = SuffixQuery local defaults = { mode = "AND", selectedStats = {}, minimapAngle = 225, } local statOptions = { { key = "strength", label = "Strength" }, { key = "agility", label = "Agility" }, { key = "stamina", label = "Stamina" }, { key = "intellect", label = "Intellect" }, { key = "spirit", label = "Spirit" }, { key = "attackPower", label = "Attack power" }, { key = "spellPower", label = "Spell power" }, { key = "healing", label = "Healing" }, { key = "crit", label = "Critical strike" }, { key = "hit", label = "Hit" }, { key = "haste", label = "Haste" }, { key = "defense", label = "Defense" }, { key = "dodge", label = "Dodge" }, { key = "parry", label = "Parry" }, { key = "block", label = "Block" }, { key = "expertise", label = "Expertise" }, } local function Suffix(term, ...) local stats = {} for index = 1, select("#", ...) do stats[select(index, ...)] = true end return { term = term, stats = stats } end -- Classic-oriented mappings based on the historical Item suffix and -- ItemRandomSuffix tables. Terms intentionally omit "of the" for AH search. local suffixes = { Suffix("Strength", "strength"), Suffix("Agility", "agility"), Suffix("Stamina", "stamina"), Suffix("Intellect", "intellect"), Suffix("Spirit", "spirit"), Suffix("Tiger", "strength", "agility"), Suffix("Beast", "strength", "agility", "stamina"), Suffix("Grove", "strength", "agility", "stamina"), Suffix("Striking", "strength", "agility", "stamina"), Suffix("Bear", "strength", "stamina"), Suffix("Gorilla", "strength", "intellect"), Suffix("Boar", "strength", "spirit"), Suffix("Monkey", "agility", "stamina"), Suffix("Falcon", "agility", "intellect"), Suffix("Wolf", "agility", "spirit"), Suffix("Eagle", "stamina", "intellect"), Suffix("Whale", "stamina", "spirit"), Suffix("Owl", "intellect", "spirit"), Suffix("Ancestor", "strength", "stamina", "crit"), Suffix("Battle", "strength", "stamina", "crit"), Suffix("Champion", "strength", "stamina", "dodge"), Suffix("Crusade", "strength", "stamina", "dodge"), Suffix("Mercenary", "strength", "stamina", "hit"), Suffix("Soldier", "strength", "stamina", "crit"), Suffix("Squire", "strength", "stamina", "hit"), Suffix("Bandit", "agility", "stamina", "crit"), Suffix("Hunt", "agility", "stamina", "crit"), Suffix("Marksman", "agility", "stamina", "hit"), Suffix("Shadow", "agility", "stamina", "crit"), Suffix("Thief", "agility", "stamina", "haste"), Suffix("Wild", "agility", "stamina", "hit"), Suffix("Elder", "stamina", "intellect", "spirit"), Suffix("Foreseer", "intellect", "crit", "haste"), Suffix("Mind", "intellect", "crit", "haste"), Suffix("Moon", "stamina", "intellect"), Suffix("Necromancer", "stamina", "intellect", "hit"), Suffix("Nightmare", "stamina", "intellect", "crit"), Suffix("Prophet", "stamina", "intellect", "spirit"), Suffix("Restoration", "stamina", "intellect", "spirit"), Suffix("Seer", "stamina", "intellect", "crit"), Suffix("Sorcerer", "stamina", "intellect"), Suffix("Sun", "stamina", "intellect", "crit"), Suffix("Vision", "stamina", "intellect", "crit"), Suffix("Eluding", "agility", "dodge"), Suffix("Blocking", "strength", "block"), Suffix("Knight", "stamina", "expertise"), Suffix("Defender", "stamina", "defense"), Suffix("Healing", "healing"), Suffix("Arcane Wrath", "spellPower"), Suffix("Fiery Wrath", "spellPower"), Suffix("Frozen Wrath", "spellPower"), Suffix("Nature's Wrath", "spellPower"), Suffix("Shadow Wrath", "spellPower"), } local function CopyDefaults(target, source) for key, value in pairs(source) do if target[key] == nil then target[key] = type(value) == "table" and CopyDefaults({}, value) or value end end return target end local function GetSelectedStats() local selected = {} for _, option in ipairs(statOptions) do if SuffixQuery.db.selectedStats[option.key] then table.insert(selected, option.key) end end return selected end local function MatchesSelection(suffix, selected, mode) if mode == "OR" then for _, stat in ipairs(selected) do if suffix.stats[stat] then return true end end return false end for _, stat in ipairs(selected) do if not suffix.stats[stat] then return false end end return true end function SuffixQuery:BuildQuery() local selected = GetSelectedStats() if #selected == 0 then return nil, "Select at least one stat." end local terms = {} for _, suffix in ipairs(suffixes) do if MatchesSelection(suffix, selected, self.db.mode) then table.insert(terms, suffix.term:lower()) end end if #terms == 0 then return nil, "No mapped suffixes match that selection." end return table.concat(terms, "|") end local function CreateBackdrop(frame) frame:SetBackdrop({ bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", tile = true, tileSize = 32, edgeSize = 32, insets = { left = 11, right = 11, top = 11, bottom = 11 }, }) frame:SetBackdropColor(0, 0, 0, 0.95) end local function CreateText(parent, text, template, point, relativeTo, relativePoint, x, y) local label = parent:CreateFontString(nil, "OVERLAY", template) label:SetText(text) label:SetPoint(point, relativeTo or parent, relativePoint or point, x or 0, y or 0) return label end function SuffixQuery:RefreshModeButtons() local isAnd = self.db.mode == "AND" self.andButton:SetText(isAnd and "AND (ON)" or "AND") self.orButton:SetText(isAnd and "OR" or "OR (ON)") end function SuffixQuery:ShowQuery() local query, message = self:BuildQuery() if not query then self.status:SetText(message) self.status:SetTextColor(1, 0.25, 0.25) return end -- Edit boxes render a literal pipe as "||". The copied value remains a -- single pipe, matching the auction-house search grammar. self.result:SetText(query:gsub("|", "||")) self.result:SetFocus() self.result:HighlightText() self.status:SetText(string.format("%d suffixes — Ctrl+C copies and closes.", select(2, query:gsub("|", "")) + 1)) self.status:SetTextColor(0.3, 1, 0.3) end function SuffixQuery:ToggleModal() if self.modal:IsShown() then self.modal:Hide() else self:RefreshModeButtons() self.modal:Show() end end function SuffixQuery:UpdateMinimapPosition() local angle = math.rad(self.db.minimapAngle) self.minimapButton:ClearAllPoints() self.minimapButton:SetPoint("CENTER", Minimap, "CENTER", math.cos(angle) * 104, math.sin(angle) * 104) end function SuffixQuery:CreateMinimapButton() local button = CreateFrame("Button", "SuffixQueryMinimapButton", Minimap) button:SetSize(62, 62) button:SetFrameStrata("MEDIUM") button:SetFrameLevel(8) local icon = button:CreateTexture(nil, "ARTWORK") icon:SetTexture("Interface\\Icons\\INV_Misc_Note_01") icon:SetSize(24, 24) icon:SetPoint("CENTER", button, "CENTER", -12, 12) icon:SetTexCoord(0.12, 0.88, 0.12, 0.88) local iconMask = button:CreateMaskTexture() iconMask:SetTexture("Interface\\Minimap\\UI-Minimap-Background", "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE") iconMask:SetAllPoints(icon) icon:AddMaskTexture(iconMask) local border = button:CreateTexture(nil, "OVERLAY") border:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder") border:SetAllPoints(button) local highlight = button:CreateTexture(nil, "HIGHLIGHT") highlight:SetTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight") highlight:SetBlendMode("ADD") highlight:SetSize(62, 62) highlight:SetPoint("CENTER", button, "CENTER", -12, 12) button.icon = icon button:RegisterForClicks("LeftButtonUp") button:RegisterForDrag("LeftButton") button:SetScript("OnClick", function() SuffixQuery:ToggleModal() end) button:SetScript("OnDragStart", function(self) self:SetScript("OnUpdate", function() local cursorX, cursorY = GetCursorPosition() local scale = Minimap:GetEffectiveScale() local centerX, centerY = Minimap:GetCenter() cursorX, cursorY = cursorX / scale, cursorY / scale SuffixQuery.db.minimapAngle = math.deg(math.atan2(cursorY - centerY, cursorX - centerX)) SuffixQuery:UpdateMinimapPosition() end) end) button:SetScript("OnDragStop", function(self) self:SetScript("OnUpdate", nil) end) button:SetScript("OnEnter", function(self) GameTooltip:SetOwner(self, "ANCHOR_LEFT") GameTooltip:AddLine("Suffix Query") GameTooltip:AddLine("Build a random-suffix search string.", 1, 1, 1, true) GameTooltip:Show() end) button:SetScript("OnLeave", GameTooltip_Hide) self.minimapButton = button self:UpdateMinimapPosition() end function SuffixQuery:CreateModal() local frame = CreateFrame("Frame", "SuffixQueryModal", UIParent, "BackdropTemplate") frame:SetSize(460, 510) frame:SetPoint("CENTER") frame:SetFrameStrata("DIALOG") frame:SetToplevel(true) frame:EnableMouse(true) frame:SetMovable(true) frame:SetClampedToScreen(true) frame:Hide() CreateBackdrop(frame) table.insert(UISpecialFrames, frame:GetName()) frame:SetScript("OnMouseDown", function(self, button) if button == "LeftButton" then self:StartMoving() end end) frame:SetScript("OnMouseUp", function(self) self:StopMovingOrSizing() end) CreateText(frame, "Suffix Query", "GameFontHighlightLarge", "TOP", frame, "TOP", 0, -22) CreateText(frame, "Select stats, choose matching logic, then generate a search string.", "GameFontNormalSmall", "TOP", frame, "TOP", 0, -48) local close = CreateFrame("Button", nil, frame, "UIPanelCloseButton") close:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -4, -4) local modeLabel = CreateText(frame, "Match suffixes containing:", "GameFontHighlight", "TOPLEFT", frame, "TOPLEFT", 24, -79) local andButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") andButton:SetSize(92, 24) andButton:SetPoint("TOPLEFT", modeLabel, "BOTTOMLEFT", 0, -7) andButton:SetScript("OnClick", function() SuffixQuery.db.mode = "AND" SuffixQuery:RefreshModeButtons() end) self.andButton = andButton local orButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") orButton:SetSize(92, 24) orButton:SetPoint("LEFT", andButton, "RIGHT", 8, 0) orButton:SetScript("OnClick", function() SuffixQuery.db.mode = "OR" SuffixQuery:RefreshModeButtons() end) self.orButton = orButton CreateText(frame, "Stats", "GameFontHighlight", "TOPLEFT", frame, "TOPLEFT", 24, -145) for index, option in ipairs(statOptions) do local column = (index - 1) % 2 local row = math.floor((index - 1) / 2) local checkbox = CreateFrame("CheckButton", nil, frame, "ChatConfigCheckButtonTemplate") checkbox:SetPoint("TOPLEFT", frame, "TOPLEFT", 28 + column * 205, -168 - row * 28) checkbox:SetChecked(self.db.selectedStats[option.key] == true) checkbox.Text:SetText(option.label) checkbox.Text:ClearAllPoints() checkbox.Text:SetPoint("LEFT", checkbox, "RIGHT", 2, 0) checkbox:SetScript("OnClick", function(self) SuffixQuery.db.selectedStats[option.key] = self:GetChecked() and true or nil end) end local generate = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") generate:SetSize(160, 28) generate:SetPoint("BOTTOM", frame, "BOTTOM", 0, 78) generate:SetText("Generate query") generate:SetScript("OnClick", function() SuffixQuery:ShowQuery() end) local result = CreateFrame("EditBox", nil, frame, "InputBoxTemplate") result:SetSize(400, 28) result:SetPoint("BOTTOM", frame, "BOTTOM", 0, 43) result:SetAutoFocus(false) result:SetFontObject("ChatFontNormal") result:SetScript("OnEscapePressed", function() frame:Hide() end) local ctrlDown = false result:SetScript("OnKeyDown", function(_, key) if key == "LCTRL" or key == "RCTRL" or key == "LMETA" or key == "RMETA" then ctrlDown = true end end) result:SetScript("OnKeyUp", function(_, key) if key == "LCTRL" or key == "RCTRL" or key == "LMETA" or key == "RMETA" then C_Timer.After(0.2, function() ctrlDown = false end) elseif ctrlDown and (key == "C" or key == "X") then C_Timer.After(0.1, function() frame:Hide() end) end end) self.result = result local status = CreateText(frame, "Generate highlights the result for copying.", "GameFontNormalSmall", "BOTTOM", frame, "BOTTOM", 0, 17) status:SetWidth(400) status:SetJustifyH("CENTER") self.status = status self.modal = frame self:RefreshModeButtons() end function SuffixQuery:Initialize() SuffixQueryDB = CopyDefaults(SuffixQueryDB or {}, defaults) self.db = SuffixQueryDB self:CreateModal() self:CreateMinimapButton() SLASH_SUFFIXQUERY1 = "/suffixquery" SlashCmdList.SUFFIXQUERY = function() SuffixQuery:ToggleModal() end end SuffixQuery:RegisterEvent("PLAYER_LOGIN") SuffixQuery:SetScript("OnEvent", function(self, event) if event == "PLAYER_LOGIN" then self:Initialize() end end)