profession pricing + territory colors

This commit is contained in:
j2 2026-09-27 20:37:42 -05:00
parent d1efb512fc
commit b7d89d90bf
6 changed files with 731 additions and 0 deletions

1
.gitignore vendored Normal file → Executable file
View File

@ -3,5 +3,6 @@
!/.gitignore
!/ClassicTerritoryColors/
!/EquippedItemLevel/
!/ProfessionPricer/
!/SetGuard/
!/SuffixQuery/

0
ClassicTerritoryColors/ClassicTerritoryColors.lua Normal file → Executable file
View File

0
ClassicTerritoryColors/ClassicTerritoryColors.toc Normal file → Executable file
View File

View File

@ -0,0 +1,709 @@
local ADDON = ...
local app = CreateFrame("Frame")
local DB
local scanButton
local cacheRevision = 0
local updateCraftPriceUI
local function say(message)
print("|cff33ff99Profession Pricer:|r " .. message)
end
local function comma(value)
local result = tostring(math.floor(tonumber(value) or 0))
while true do
local nextResult, count = result:gsub("^(-?%d+)(%d%d%d)", "%1,%2")
result = nextResult
if count == 0 then return result end
end
end
local GOLD = "|TInterface\\MoneyFrame\\UI-GoldIcon:0:0:2:0|t"
local SILVER = "|TInterface\\MoneyFrame\\UI-SilverIcon:0:0:2:0|t"
local COPPER = "|TInterface\\MoneyFrame\\UI-CopperIcon:0:0:2:0|t"
local function money(copper)
copper = math.max(0, math.floor(tonumber(copper) or 0))
local gold = math.floor(copper / 10000)
local silver = math.floor((copper % 10000) / 100)
local coins = copper % 100
local parts = {}
if gold > 0 then parts[#parts + 1] = gold .. GOLD end
if silver > 0 or gold > 0 then parts[#parts + 1] = silver .. SILVER end
parts[#parts + 1] = coins .. COPPER
return table.concat(parts, " ")
end
local function age(timestamp)
if not timestamp then return "unknown" end
local seconds = math.max(0, time() - timestamp)
if seconds < 60 then return "<1m" end
if seconds < 3600 then return math.floor(seconds / 60) .. "m" end
if seconds < 86400 then return math.floor(seconds / 3600) .. "h" end
return math.floor(seconds / 86400) .. "d"
end
local function countPrices()
local count = 0
for _ in pairs(DB.prices) do count = count + 1 end
return count
end
local function cachedPrice(itemID)
local entry = DB and DB.prices and DB.prices[tonumber(itemID)]
return entry and tonumber(entry.price) or nil
end
local function savePrice(itemID, price, source)
itemID = tonumber(itemID)
price = tonumber(price)
if not itemID or not price or price <= 0 then return end
price = math.floor(price)
if price <= 0 then return end
local old = DB.prices[itemID]
DB.prices[itemID] = {price = price, updated = time(), source = source}
if not old or old.price ~= price then cacheRevision = cacheRevision + 1 end
end
local function lowestCommodityPrice(itemID)
if not (C_AuctionHouse and C_AuctionHouse.GetNumCommoditySearchResults and C_AuctionHouse.GetCommoditySearchResultInfo) then return nil end
local ok, count = pcall(C_AuctionHouse.GetNumCommoditySearchResults, itemID)
if not ok or not count then return nil end
local best
for i = 1, count do
local success, result = pcall(C_AuctionHouse.GetCommoditySearchResultInfo, itemID, i)
local price = success and result and tonumber(result.unitPrice)
if price and price > 0 and (not best or price < best) then best = price end
end
return best
end
local function lowestItemPrice(itemKey)
if not (C_AuctionHouse and C_AuctionHouse.GetNumItemSearchResults and C_AuctionHouse.GetItemSearchResultInfo) then return nil end
local ok, count = pcall(C_AuctionHouse.GetNumItemSearchResults, itemKey)
if not ok or not count then return nil end
local best
for i = 1, count do
local success, result = pcall(C_AuctionHouse.GetItemSearchResultInfo, itemKey, i)
local price = success and result and tonumber(result.buyoutAmount)
if price and price > 0 and (not best or price < best) then best = price end
end
return best
end
local browse = {active = false, token = 0}
local updateScanButton
local function countSeen()
local count = 0
for _ in pairs(browse.seen or {}) do count = count + 1 end
return count
end
local function endBrowse(reason, completed)
if not browse.active then return end
browse.active = false
browse.token = browse.token + 1
local count = countSeen()
for itemID, price in pairs(browse.prices) do savePrice(itemID, price, "browse") end
if completed then
DB.lastBrowseScan = time()
DB.lastBrowseItems = count
end
updateScanButton()
say((completed and "AH browse scan complete: " or "AH browse scan stopped: ") .. comma(count) .. " item summaries cached (" .. reason .. ").")
end
local function cacheBrowseResults()
if not (C_AuctionHouse and C_AuctionHouse.GetBrowseResults) then return 0, 0 end
local ok, results = pcall(C_AuctionHouse.GetBrowseResults)
if not ok or type(results) ~= "table" then return 0, 0 end
local added = 0
for _, result in ipairs(results) do
local key = result.itemKey
local itemID = key and tonumber(key.itemID)
local price = tonumber(result.minPrice)
if itemID and price and price > 0 then
if browse.active then
local signature = itemID .. ":" .. tostring(key.itemLevel or 0) .. ":" .. tostring(key.itemSuffix or 0) .. ":" .. tostring(key.battlePetSpeciesID or 0)
if not browse.seen[signature] then
browse.seen[signature] = true
added = added + 1
end
local prior = browse.prices[itemID]
if not prior or price < prior then browse.prices[itemID] = price end
else
local old = DB.prices[itemID]
if not old or not old.price or price < old.price
or (old.source == "browse" and old.updated and time() - old.updated > 60) then
savePrice(itemID, price, "browse")
end
end
end
end
return added, #results
end
local function requestMoreBrowse(token)
if not browse.active or browse.token ~= token then return end
if C_AuctionHouse.IsThrottledMessageSystemReady then
local ok, ready = pcall(C_AuctionHouse.IsThrottledMessageSystemReady)
if ok and not ready then
C_Timer.After(0.75, function() requestMoreBrowse(token) end)
return
end
end
browse.waiting = true
local ok = pcall(C_AuctionHouse.RequestMoreBrowseResults)
if not ok then endBrowse("request failed", false) end
end
local function browseResultsUpdated()
local added, total = cacheBrowseResults()
if not browse.active or not browse.waiting then return end
browse.waiting = false
browse.lastActivity = time()
browse.pages = browse.pages + 1
updateScanButton()
if C_AuctionHouse.HasFullBrowseResults then
local ok, full = pcall(C_AuctionHouse.HasFullBrowseResults)
if ok and full then endBrowse("full results", true); return end
end
if total == browse.lastCount and added == 0 then
browse.stable = browse.stable + 1
else
browse.stable = 0
end
browse.lastCount = total
if browse.stable >= 3 then endBrowse("no more results", true); return end
if not C_AuctionHouse.RequestMoreBrowseResults then
endBrowse("pagination unavailable", false)
return
end
local token = browse.token
C_Timer.After(0.35, function() requestMoreBrowse(token) end)
end
local function browseWatchdog(token)
if not browse.active or browse.token ~= token then return end
if time() - browse.lastActivity >= 20 then
endBrowse("no response from Auction House", false)
else
C_Timer.After(5, function() browseWatchdog(token) end)
end
end
local function startBrowseScan()
if browse.active then say("AH browse scan is already running (" .. comma(countSeen()) .. " summaries found).") return end
if not (AuctionHouseFrame and AuctionHouseFrame:IsShown()) then say("Open the Auction House before scanning.") return end
if not (C_AuctionHouse and C_AuctionHouse.SendBrowseQuery and C_AuctionHouse.GetBrowseResults) then
say("AH browse scanning API is unavailable on this client.")
return
end
browse.token = browse.token + 1
browse.active = true
browse.waiting = true
browse.seen = {}
browse.prices = {}
browse.pages = 0
browse.stable = 0
browse.lastCount = 0
browse.lastActivity = time()
updateScanButton()
local filters = {}
if Enum and Enum.AuctionHouseFilter then
for _, name in ipairs({"PoorQuality", "CommonQuality", "UncommonQuality", "RareQuality", "EpicQuality", "LegendaryQuality", "ArtifactQuality"}) do
local filter = Enum.AuctionHouseFilter[name]
if filter ~= nil then filters[#filters + 1] = filter end
end
end
local sorts = {}
if Enum and Enum.AuctionHouseSortOrder then
if Enum.AuctionHouseSortOrder.Price ~= nil then
sorts[#sorts + 1] = {sortOrder = Enum.AuctionHouseSortOrder.Price, reverseSort = false}
end
if Enum.AuctionHouseSortOrder.Name ~= nil then
sorts[#sorts + 1] = {sortOrder = Enum.AuctionHouseSortOrder.Name, reverseSort = false}
end
end
local ok, err = pcall(C_AuctionHouse.SendBrowseQuery, {searchString = "", sorts = sorts, filters = filters})
if not ok then endBrowse("query failed: " .. tostring(err), false); return end
say("starting AH browse scan...")
local token = browse.token
C_Timer.After(5, function() browseWatchdog(token) end)
end
updateScanButton = function()
if not scanButton then return end
scanButton:SetText(browse.active and "Scanning..." or "Scan AH")
if browse.active then
scanButton.status:SetText("|cffffff66" .. comma(countSeen()) .. " found|r")
elseif DB.lastBrowseScan then
scanButton.status:SetText("|cff33ff66Last: " .. age(DB.lastBrowseScan) .. " ago|r")
else
scanButton.status:SetText("|cffaaaaaaNot scanned|r")
end
end
local function createScanButton()
if scanButton or not AuctionHouseFrame then return end
local button = CreateFrame("Button", "ProfessionPricerScanButton", AuctionHouseFrame, "UIPanelButtonTemplate")
button:SetSize(92, 24)
if AuctionHouseFrameAuctionsTab then
button:SetPoint("LEFT", AuctionHouseFrameAuctionsTab, "RIGHT", 8, 0)
elseif AuctionHouseFrame.BuyTab then
button:SetPoint("BOTTOMLEFT", AuctionHouseFrame, "BOTTOMLEFT", 310, 4)
else
button:SetPoint("BOTTOMLEFT", AuctionHouseFrame, "BOTTOMLEFT", 305, 4)
end
button:SetFrameLevel((AuctionHouseFrame:GetFrameLevel() or 1) + 20)
local status = button:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
status:SetPoint("TOP", button, "BOTTOM", 0, -1)
button.status = status
button:SetScript("OnClick", startBrowseScan)
button:SetScript("OnEnter", function(self)
GameTooltip:SetOwner(self, "ANCHOR_TOP")
GameTooltip:SetText("Profession Pricer AH Scan")
if browse.active then
GameTooltip:AddLine(comma(countSeen()) .. " item summaries discovered", 1, 1, 1)
elseif DB.lastBrowseScan then
GameTooltip:AddLine("Last scan: " .. age(DB.lastBrowseScan) .. " ago", 1, 1, 1)
GameTooltip:AddLine(comma(DB.lastBrowseItems or 0) .. " item summaries cached", 1, 1, 1)
else
GameTooltip:AddLine("No scan completed yet.", 1, 1, 1)
end
GameTooltip:Show()
end)
button:SetScript("OnLeave", function() GameTooltip:Hide() end)
scanButton = button
updateScanButton()
end
local replicate = {active = false, token = 0}
local replicationUpdated
local function finishReplication()
if not replicate.active then return end
local count = 0
for itemID, price in pairs(replicate.prices) do
savePrice(itemID, price, "replicate")
count = count + 1
end
DB.lastFullScan = time()
DB.lastFullItems = count
DB.lastFullAuctions = replicate.auctions
replicate.active = false
replicate.token = replicate.token + 1
say("full AH scan complete: " .. comma(count) .. " item prices from " .. comma(replicate.auctions) .. " auctions.")
end
local function processReplicationBatch(token)
if not replicate.active or replicate.token ~= token then return end
local stop = math.min(replicate.index + 249, replicate.total - 1)
while replicate.index <= stop do
local ok, _, _, quantity, _, _, _, _, _, _, buyout, _, _, _, _, _, _, itemID = pcall(C_AuctionHouse.GetReplicateItemInfo, replicate.index)
if ok and itemID and tonumber(buyout) and buyout > 0 then
quantity = math.max(1, tonumber(quantity) or 1)
local unitPrice = math.floor(buyout / quantity)
if unitPrice > 0 and (not replicate.prices[itemID] or unitPrice < replicate.prices[itemID]) then
replicate.prices[itemID] = unitPrice
end
replicate.auctions = replicate.auctions + 1
end
replicate.index = replicate.index + 1
end
if replicate.index >= replicate.total then
finishReplication()
else
C_Timer.After(0.02, function() processReplicationBatch(token) end)
end
end
replicationUpdated = function()
if not replicate.active or not replicate.waiting then return end
local ok, count = pcall(C_AuctionHouse.GetNumReplicateItems)
if not ok then
replicate.active = false
say("could not read replicated AH snapshot.")
return
end
if not count or count <= 0 then return end
replicate.waiting = false
replicate.total = count
replicate.index = 0
say("AH snapshot received: " .. comma(count) .. " auctions; building price cache...")
processReplicationBatch(replicate.token)
end
local function startFullScan()
if replicate.active then say("full AH scan is already running.") return end
if not (AuctionHouseFrame and AuctionHouseFrame:IsShown()) then say("Open the Auction House before scanning.") return end
if not (C_AuctionHouse and C_AuctionHouse.ReplicateItems and C_AuctionHouse.GetNumReplicateItems and C_AuctionHouse.GetReplicateItemInfo) then
say("AH replication API is unavailable on this client.")
return
end
replicate.token = replicate.token + 1
replicate.active = true
replicate.waiting = true
replicate.index = 0
replicate.total = 0
replicate.auctions = 0
replicate.prices = {}
local ok, err = pcall(C_AuctionHouse.ReplicateItems)
if not ok then
replicate.active = false
say("AH snapshot request failed: " .. tostring(err))
return
end
say("requesting full AH snapshot...")
local token = replicate.token
local tries = 0
local function poll()
if not replicate.active or not replicate.waiting or replicate.token ~= token then return end
tries = tries + 1
replicationUpdated()
if not replicate.waiting then return end
if tries >= 30 then
replicate.active = false
say("no replicated AH snapshot arrived; scan stopped.")
else
C_Timer.After(0.5, poll)
end
end
C_Timer.After(0.5, poll)
end
local craft = {owner = nil, frame = nil, reagentLines = {}, recipeID = nil, revision = -1}
local function selectedRecipe()
local form = ProfessionsFrame and ProfessionsFrame.CraftingPage and ProfessionsFrame.CraftingPage.SchematicForm
if not form or type(form.GetRecipeInfo) ~= "function" then return nil, form end
local ok, info = pcall(form.GetRecipeInfo, form)
if not ok or not info then return nil, form end
return info.recipeID, form
end
local function recipePricing(recipeID)
if not (recipeID and C_TradeSkillUI and type(C_TradeSkillUI.GetRecipeSchematic) == "function") then return nil end
local ok, schematic = pcall(C_TradeSkillUI.GetRecipeSchematic, recipeID, false)
if not ok or not schematic then return nil end
local result = {cost = 0, reagents = {}, outputID = schematic.outputItemID, outputQty = tonumber(schematic.quantityMin) or 1}
for _, slot in ipairs(schematic.reagentSlotSchematics or {}) do
local quantity = tonumber(slot.quantityRequired) or 0
local reagent = slot.reagents and slot.reagents[1]
if reagent and reagent.itemID and quantity > 0 then
local price = cachedPrice(reagent.itemID)
result.reagents[#result.reagents + 1] = {quantity = quantity, price = price}
if price then result.cost = result.cost + price * quantity else result.missing = true end
end
end
local outputPrice = result.outputID and cachedPrice(result.outputID)
result.outputValue = outputPrice and outputPrice * result.outputQty or nil
return result
end
local SORT_MODES = {"default", "cost", "profit"}
local SORT_LABELS = {default = "Default", cost = "Craft Cost: low first", profit = "Profit/Craft: high first"}
local recipeSort = {scrollBox = nil, button = nil, revision = -1}
local function sortedRecipeProvider(source)
if not DB or DB.sortMode == "default" or not source or type(CreateTreeDataProvider) ~= "function"
or type(source.EnumerateEntireRange) ~= "function" then
return source
end
local rows, seen = {}, {}
local ok = pcall(function()
for _, node in source:EnumerateEntireRange() do
local data = node:GetData()
local info = data and data.recipeInfo
if info and info.recipeID and not info.favoritesInstance and not seen[info.recipeID] then
seen[info.recipeID] = true
local pricing = recipePricing(info.recipeID)
local score
if pricing and not pricing.missing then
if DB.sortMode == "cost" then
score = pricing.cost
elseif pricing.outputValue then
score = pricing.outputValue - pricing.cost
end
end
rows[#rows + 1] = {info = info, score = score, learned = info.learned ~= false}
end
end
end)
if not ok or #rows == 0 then return source end
table.sort(rows, function(a, b)
-- Keep recipes the character cannot craft after learned recipes.
if a.learned ~= b.learned then return a.learned end
if a.score == nil and b.score ~= nil then return false end
if b.score == nil and a.score ~= nil then return true end
if a.score ~= nil and b.score ~= nil and a.score ~= b.score then
if DB.sortMode == "cost" then return a.score < b.score end
return a.score > b.score
end
local aName = (a.info.name or ""):lower()
local bName = (b.info.name or ""):lower()
if aName ~= bName then return aName < bName end
return a.info.recipeID < b.info.recipeID
end)
local built, flat = pcall(function()
local provider = CreateTreeDataProvider()
local root = provider:GetRootNode()
for _, row in ipairs(rows) do root:Insert({recipeInfo = row.info, order = 0}) end
return provider
end)
return built and flat or source
end
local function refreshSortedRecipes()
local page = ProfessionsFrame and ProfessionsFrame.CraftingPage
if not page or not page.professionInfo or type(page.Init) ~= "function" then return end
recipeSort.revision = cacheRevision
local ok, err = pcall(page.Init, page, page.professionInfo)
if not ok then say("could not refresh recipe order: " .. tostring(err)) end
end
local function setSortMode(mode)
if mode ~= "default" and mode ~= "cost" and mode ~= "profit" then
say("Use /pp sort default, /pp sort cost, or /pp sort profit.")
return
end
DB.sortMode = mode
if recipeSort.button then recipeSort.button:SetText("Sort: " .. SORT_LABELS[mode]) end
refreshSortedRecipes()
end
local function ensureRecipeSort()
local page = ProfessionsFrame and ProfessionsFrame.CraftingPage
local list = page and page.RecipeList
local scrollBox = list and list.ScrollBox
if not scrollBox or type(scrollBox.SetDataProvider) ~= "function" or type(CreateTreeDataProvider) ~= "function" then return end
if recipeSort.scrollBox ~= scrollBox then
local nativeSetDataProvider = scrollBox.SetDataProvider
scrollBox.SetDataProvider = function(self, provider, ...)
local sorted, displayProvider = pcall(sortedRecipeProvider, provider)
recipeSort.revision = cacheRevision
return nativeSetDataProvider(self, sorted and displayProvider or provider, ...)
end
recipeSort.scrollBox = scrollBox
if recipeSort.button then recipeSort.button:Hide() end
recipeSort.button = nil
end
if not recipeSort.button then
local button = CreateFrame("Button", nil, list, "UIPanelButtonTemplate")
button:SetSize(260, 22)
button:SetPoint("TOPLEFT", list, "BOTTOMLEFT", 6, -10)
button:SetFrameLevel((list:GetFrameLevel() or 1) + 5)
button:SetScript("OnClick", function()
local current = DB.sortMode or "default"
for index, mode in ipairs(SORT_MODES) do
if mode == current then
setSortMode(SORT_MODES[(index % #SORT_MODES) + 1])
return
end
end
setSortMode("default")
end)
button:SetScript("OnEnter", function(self)
GameTooltip:SetOwner(self, "ANCHOR_TOP")
GameTooltip:SetText("Profession Pricer Recipe Sort")
GameTooltip:AddLine("Click to cycle through default order, lowest AH craft cost, and highest estimated profit per craft.", 1, 1, 1, true)
GameTooltip:AddLine("Unlearned and unpriced recipes follow priced learned recipes.", .8, .8, .8, true)
GameTooltip:Show()
end)
button:SetScript("OnLeave", function() GameTooltip:Hide() end)
recipeSort.button = button
button:SetText("Sort: " .. SORT_LABELS[DB.sortMode or "default"])
if DB.sortMode ~= "default" then refreshSortedRecipes() end
end
end
local function hideCraft()
if craft.frame then craft.frame:Hide() end
for _, line in ipairs(craft.reagentLines) do line:Hide() end
craft.recipeID = nil
end
local function ensureCraftFrame(form)
if craft.owner == form and craft.frame then return end
hideCraft()
craft.owner = form
craft.reagentLines = {}
local frame = CreateFrame("Frame", nil, form)
frame:SetSize(190, 42)
frame:SetPoint("TOPRIGHT", form, "TOPRIGHT", -18, -48)
frame:SetFrameLevel((form:GetFrameLevel() or 1) + 30)
local cost = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
cost:SetPoint("TOPRIGHT", 0, 0)
cost:SetWidth(190)
cost:SetJustifyH("RIGHT")
local value = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
value:SetPoint("TOPRIGHT", cost, "BOTTOMRIGHT", 0, -2)
value:SetWidth(190)
value:SetJustifyH("RIGHT")
local margin = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
margin:SetPoint("TOPRIGHT", value, "BOTTOMRIGHT", 0, -2)
margin:SetWidth(190)
margin:SetJustifyH("RIGHT")
frame.cost, frame.value, frame.margin = cost, value, margin
craft.frame = frame
end
local function showReagentPrices(form, pricing)
for _, line in ipairs(craft.reagentLines) do line:Hide() end
for index, reagent in ipairs(pricing.reagents) do
local line = craft.reagentLines[index]
if not line then
line = form:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
craft.reagentLines[index] = line
end
line:ClearAllPoints()
line:SetPoint("TOPLEFT", form, "TOPLEFT", 190, -159 - ((index - 1) * 52))
line:SetWidth(145)
line:SetJustifyH("LEFT")
if reagent.price then
if reagent.quantity > 1 then
line:SetText("|cff33ff99" .. money(reagent.price) .. " ea / " .. money(reagent.price * reagent.quantity) .. " total|r")
else
line:SetText("|cff33ff99" .. money(reagent.price) .. " each|r")
end
else
line:SetText("|cff888888N/A|r")
end
line:Show()
end
end
updateCraftPriceUI = function()
local recipeID, form = selectedRecipe()
if not recipeID or not form or not form:IsShown() then hideCraft(); return end
local pricing = recipePricing(recipeID)
if not pricing then hideCraft(); return end
ensureCraftFrame(form)
craft.frame:Show()
craft.frame.cost:SetText(pricing.missing and "AH Craft Cost: |cffaaaaaaN/A|r" or "AH Craft Cost: " .. money(pricing.cost))
if pricing.outputValue then
craft.frame.value:SetText("AH Result Value: " .. money(pricing.outputValue))
if pricing.missing then
craft.frame.margin:SetText("")
else
local difference = pricing.outputValue - pricing.cost
if difference >= 0 then
craft.frame.margin:SetText("|cff33ff66Avg Profit / Craft: +" .. money(difference) .. "|r")
else
craft.frame.margin:SetText("|cffff5555Avg Loss / Craft: -" .. money(-difference) .. "|r")
end
end
else
craft.frame.value:SetText("AH Result Value: |cffaaaaaaN/A|r")
craft.frame.margin:SetText("")
end
showReagentPrices(form, pricing)
craft.recipeID = recipeID
craft.revision = cacheRevision
end
local function status()
say(comma(countPrices()) .. " cached item prices.")
if browse.active then
say("browse scan: " .. comma(countSeen()) .. " summaries across " .. comma(browse.pages) .. " result pages.")
elseif DB.lastBrowseScan then
say("last browse scan: " .. comma(DB.lastBrowseItems or 0) .. " summaries, " .. age(DB.lastBrowseScan) .. " ago.")
end
if replicate.active then
say(replicate.waiting and "full scan: waiting for snapshot." or ("full scan: " .. comma(replicate.index) .. "/" .. comma(replicate.total) .. " auctions processed."))
elseif DB.lastFullScan then
say("last full scan: " .. comma(DB.lastFullItems or 0) .. " items, " .. age(DB.lastFullScan) .. " ago.")
end
end
SLASH_PROFESSIONPRICER1 = "/pp"
SLASH_PROFESSIONPRICER2 = "/pricer"
SlashCmdList.PROFESSIONPRICER = function(raw)
local command = (raw or ""):lower():match("^%s*(.-)%s*$")
if command == "scan" then
startBrowseScan()
elseif command == "fullscan" then
startFullScan()
elseif command == "status" then
status()
elseif command:match("^sort%s+") then
setSortMode(command:match("^sort%s+(%S+)$"))
else
say("/pp scan - browse the Auction House for prices")
say("/pp fullscan - request a full AH snapshot when available")
say("/pp status - show cached prices and scan progress")
say("/pp sort default|cost|profit - order recipes across the profession")
end
end
app:SetScript("OnEvent", function(_, event, ...)
if event == "ADDON_LOADED" then
if ... ~= ADDON then return end
ProfessionPricerDB = ProfessionPricerDB or {}
DB = ProfessionPricerDB
DB.prices = DB.prices or {}
if not SORT_LABELS[DB.sortMode] then DB.sortMode = "default" end
ensureRecipeSort()
say("loaded. Use /pp help for commands.")
elseif event == "TRADE_SKILL_SHOW" or event == "TRADE_SKILL_LIST_UPDATE" or event == "TRADE_SKILL_DATA_SOURCE_CHANGED" then
C_Timer.After(0.1, function() ensureRecipeSort(); updateCraftPriceUI() end)
elseif event == "AUCTION_HOUSE_SHOW" then
C_Timer.After(0.1, createScanButton)
elseif event == "AUCTION_HOUSE_CLOSED" then
if browse.active then endBrowse("Auction House closed", false) end
elseif event == "AUCTION_HOUSE_BROWSE_RESULTS_UPDATED" or event == "AUCTION_HOUSE_BROWSE_RESULTS_ADDED" then
browseResultsUpdated()
elseif event == "COMMODITY_SEARCH_RESULTS_UPDATED" then
local itemID = ...
local price = lowestCommodityPrice(itemID)
if price then savePrice(itemID, price, "commodity") end
elseif event == "ITEM_SEARCH_RESULTS_UPDATED" then
local itemKey = ...
if itemKey and itemKey.itemID then
local price = lowestItemPrice(itemKey)
if price then savePrice(itemKey.itemID, price, "item") end
end
elseif event == "REPLICATE_ITEM_LIST_UPDATE" then
replicationUpdated()
end
end)
local elapsedSinceCraftCheck = 0
app:SetScript("OnUpdate", function(_, elapsed)
if not DB then return end
elapsedSinceCraftCheck = elapsedSinceCraftCheck + elapsed
if elapsedSinceCraftCheck < 0.5 then return end
elapsedSinceCraftCheck = 0
ensureRecipeSort()
if DB.sortMode ~= "default" and recipeSort.scrollBox and recipeSort.revision ~= cacheRevision then
refreshSortedRecipes()
end
local recipeID, form = selectedRecipe()
if recipeID and form and form:IsShown() then
if recipeID ~= craft.recipeID or form ~= craft.owner or cacheRevision ~= craft.revision or not craft.frame or not craft.frame:IsShown() then
updateCraftPriceUI()
end
elseif craft.frame and craft.frame:IsShown() then
hideCraft()
end
end)
app:RegisterEvent("ADDON_LOADED")
app:RegisterEvent("AUCTION_HOUSE_SHOW")
app:RegisterEvent("AUCTION_HOUSE_CLOSED")
app:RegisterEvent("AUCTION_HOUSE_BROWSE_RESULTS_UPDATED")
app:RegisterEvent("AUCTION_HOUSE_BROWSE_RESULTS_ADDED")
app:RegisterEvent("COMMODITY_SEARCH_RESULTS_UPDATED")
app:RegisterEvent("ITEM_SEARCH_RESULTS_UPDATED")
app:RegisterEvent("REPLICATE_ITEM_LIST_UPDATE")
app:RegisterEvent("TRADE_SKILL_SHOW")
app:RegisterEvent("TRADE_SKILL_LIST_UPDATE")
app:RegisterEvent("TRADE_SKILL_DATA_SOURCE_CHANGED")

View File

@ -0,0 +1,8 @@
## Interface: 16001
## Title: Profession Pricer
## Notes: Auction House price scanning and profession craft pricing for WoW Forever.
## Author: Local
## Version: 1.1
## SavedVariables: ProfessionPricerDB
ProfessionPricer.lua

13
ProfessionPricer/README.md Executable file
View File

@ -0,0 +1,13 @@
# Profession Pricer
Standalone Auction House price scanner and profession price display for WoW Forever / Classic Beta. Prices start empty and are saved in `ProfessionPricerDB` as they are learned.
- Open the Auction House and click **Scan AH**, or use `/pp scan`, to browse all available item summaries.
- `/pp fullscan` requests a replicated auction snapshot when that client API is available.
- Normal Auction House browse and search results also update cached prices.
- Select a recipe in the profession window to see reagent prices, total cost, result value, and estimated profit or loss. Missing prices appear as `N/A`.
- Click **Sort** above the recipe list to cycle through the default category view, lowest AH craft cost, and highest estimated profit per craft. The price modes show one list across the whole profession. Unlearned recipes and recipes with missing prices follow priced learned recipes.
- `/pp sort default`, `/pp sort cost`, and `/pp sort profit` select a sort mode directly. The choice is saved.
- `/pp status` shows scan progress and cache size. `/pp help` lists commands.
Auction prices are observed buyout prices, not guaranteed sale values. The display is an estimate and does not include Auction House fees or deposits.