Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Constants.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ local Constants = {}
addon.Constants = Constants

Constants.TITLEBAR_HEIGHT = 30
Constants.TABLE_ROW_HEIGHT = 24
Constants.TABLE_ROW_HEIGHT = 30
Constants.TABLE_HEADER_HEIGHT = 32
Constants.TABLE_CELL_PADDING = 8
Constants.MAX_WINDOW_HEIGHT = 500
20 changes: 20 additions & 0 deletions Core.lua
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,26 @@ function Core:OnEnable()
self:Render()
end
)
self:RegisterBucketEvent(
{ "PROFESSION_EQUIPMENT_CHANGED" },
2,
function()
-- Note: ClearProgressCache() is intentionally omitted -- profGearScore
-- is not stored in progressCache, so clearing it would be a no-op here.
Data:ScanProfessionEquipment()
self:Render()
end
)
self:RegisterBucketEvent(
{ "GET_ITEM_INFO_RECEIVED" },
1,
function()
if Data:NeedsProfessionEquipmentRescan() then
Data:ScanProfessionEquipment()
self:Render()
end
end
)

Data:ScanAll()
self:Render()
Expand Down
124 changes: 124 additions & 0 deletions Data.lua
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,28 @@ function Data:DeleteCharacter(characterOrGUID)
self:ClearProgressCache(true)
end

local PROF_GEAR_ILVL_TO_RANK = {
[2] = { [180]=1, [186]=2, [192]=3, [199]=4, [206]=5 }, -- Uncommon (green)
[3] = { [206]=1, [212]=2, [218]=3, [225]=4, [232]=5 }, -- Rare (blue)
[4] = { [232]=1, [238]=2, [244]=3, [251]=4, [258]=5 }, -- Epic (purple)
}

local PROF_GEAR_TIER_OFFSET = { [2]=0, [3]=5, [4]=10 }

--- Returns score (1-15) and craftingRank (1-5), or 0, 0 if unrecognised.
local function ComputeSlotScore(itemQuality, itemLevel)
local rankTable = PROF_GEAR_ILVL_TO_RANK[itemQuality]
if not rankTable then return 0, 0 end
local rank = rankTable[itemLevel]
if not rank then return 0, 0 end
return PROF_GEAR_TIER_OFFSET[itemQuality] + rank, rank
end

local PROF_SLOT_GROUPS = {
{ prefix = "PROF0", slots = {"TOOLSLOT", "GEAR0SLOT", "GEAR1SLOT"} },
{ prefix = "PROF1", slots = {"TOOLSLOT", "GEAR0SLOT", "GEAR1SLOT"} },
}

---Update the current character.
function Data:ScanAll()
self:ScanCharacterInfo()
Expand All @@ -502,6 +524,108 @@ function Data:ScanAll()
self:ScanQuests()
self:ScanProfessions()
self:ScanCalendar()
self:ScanProfessionEquipment()
end

--- Returns true if any profession has unscanned or pending gear slots.
function Data:NeedsProfessionEquipmentRescan()
local character = self:GetCharacter()
if not character then return false end
for _, cp in ipairs(character.professions or {}) do
if cp.equipment == nil then return true end
for i = 1, 3 do
if cp.equipment[i] and cp.equipment[i].pending then return true end
end
end
return false
end

--- Scan profession gear slots for the current character and store results in AceDB.
function Data:ScanProfessionEquipment()
if self:IsInChatMessagingLockdown() then return end
if InCombatLockdown and InCombatLockdown() then return end
local character = self:GetCharacter()
if not character then return end

local professionIndex1, professionIndex2 = GetProfessions()
local professionIndexes = { professionIndex1, professionIndex2 }

for _, group in ipairs(PROF_SLOT_GROUPS) do
-- Determine which profession owns this slot group by matching the tool slot item's
-- subType (e.g. "Enchanting") against GetProfessionInfo name (position 1).
-- GetProfessions() ordering does not reliably map to PROF0/PROF1 slot ordering.
local toolSlotID = GetInventorySlotInfo(group.prefix .. "TOOLSLOT")
local toolLink = GetInventoryItemLink("player", toolSlotID)
local toolSubType = toolLink and select(7, C_Item.GetItemInfo(toolLink))

local skillLineID
for _, professionIndex in ipairs(professionIndexes) do
if professionIndex then
local profName, _, _, _, _, _, slID = GetProfessionInfo(professionIndex)
if profName and toolSubType and profName == toolSubType then
skillLineID = slID
break
end
end
end

-- If skillLineID is still nil (no tool equipped or tool data uncached), skip this
-- slot group entirely. No fallback to index-based mapping to avoid writing gear
-- to the wrong profession row.

if skillLineID then
-- Multiple characterProfessions can share the same skillLineID (one per expansion).
-- The same physical gear slots apply to all of them, so write equipment to all matches.
local matchingProfessions = Utils:TableFilter(character.professions, function(cp)
local variant = self:GetSkillLineVariantByID(cp.skillLineVariantID)
return variant and variant.skillLineID == skillLineID
end)

if #matchingProfessions > 0 then
local equipment = {}
local totalScore = 0

for slotIndex, slotSuffix in ipairs(group.slots) do
local slotName = group.prefix .. slotSuffix
local slotID = GetInventorySlotInfo(slotName)
local itemLink = GetInventoryItemLink("player", slotID)

if itemLink then
-- C_Item.GetItemInfo return order (confirmed in-game):
-- 1=name, 2=link, 3=quality, 4=itemLevel, 5=requiredLevel,
-- 6=type, 7=subType, 8=stackCount, 9=equipLoc, 10=icon, ...
local name, _, quality, itemLevel, _, _, _, _, _, icon, _, _, _, _, itemExpansionID =
C_Item.GetItemInfo(itemLink)
if name then
local score, rank = ComputeSlotScore(quality, itemLevel)
equipment[slotIndex] = {
itemLink = itemLink,
itemQuality = quality,
itemLevel = itemLevel,
iconFileID = icon,
score = score,
craftingRank = rank,
itemExpansionID = itemExpansionID,
}
totalScore = totalScore + score
else
-- Item data not yet cached; store sentinel so GET_ITEM_INFO_RECEIVED
-- can distinguish this from a genuinely empty slot.
equipment[slotIndex] = { itemLink = itemLink, pending = true }
end
end
-- nil entry in equipment[slotIndex] = scanned, slot genuinely empty
end

-- Write the same gear data to all expansion rows for this profession
Utils:TableForEach(matchingProfessions, function(cp)
cp.equipment = equipment
cp.profGearScore = totalScore
end)
end
-- if no matches: profession not in DB yet; equipment stays nil (unscanned)
end
end
end

--- Scan currencies for a character.
Expand Down
100 changes: 100 additions & 0 deletions Main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,73 @@ function Main:ToggleWindow()
self:Render()
end

local EMPTY_SLOT_TEXTURE = 4760248 -- confirmed in-game via GetInventorySlotInfo [2]
local GEAR_ICON_SIZE = 24

local function SlotBelongsToExpansion(slot, expansionID)
return slot.itemExpansionID == expansionID
end

local function GearCellIcons(characterProfession, skillLineVariantID)
local function onLeave() GameTooltip:Hide() end

if characterProfession.equipment == nil then
-- Never scanned: 3 dimmed empty slot icons with no overlay
local onEnter = function(frame)
GameTooltip:SetOwner(frame, "ANCHOR_RIGHT")
GameTooltip:SetText("Not yet scanned", 1, 1, 1)
GameTooltip:AddLine("Log in on this character to scan their gear.", nil, nil, nil, true)
GameTooltip:Show()
end
return {
{ iconFileID = EMPTY_SLOT_TEXTURE, unscanned = true, size = GEAR_ICON_SIZE, onEnter = onEnter, onLeave = onLeave },
{ iconFileID = EMPTY_SLOT_TEXTURE, unscanned = true, size = GEAR_ICON_SIZE, onEnter = onEnter, onLeave = onLeave },
{ iconFileID = EMPTY_SLOT_TEXTURE, unscanned = true, size = GEAR_ICON_SIZE, onEnter = onEnter, onLeave = onLeave },
}
end

local variant = Data:GetSkillLineVariantByID(skillLineVariantID)
local expansionID = variant and variant.expansionID

local icons = {}
for i = 1, 3 do
local slot = characterProfession.equipment[i]
local slotBelongs = slot and not slot.pending and expansionID and SlotBelongsToExpansion(slot, expansionID)
if slotBelongs then
-- Item equipped and belongs to this expansion: full brightness icon + quality star overlay
local itemLink = slot.itemLink
local qualityColor = ITEM_QUALITY_COLORS[slot.itemQuality]
icons[i] = {
iconFileID = slot.iconFileID,
overlayAtlas = slot.craftingRank > 0 and ("Professions-ChatIcon-Quality-Tier" .. slot.craftingRank) or nil,
borderColor = qualityColor and {r = qualityColor.r, g = qualityColor.g, b = qualityColor.b},
size = GEAR_ICON_SIZE,
onEnter = function(frame)
GameTooltip:SetOwner(frame, "ANCHOR_RIGHT")
GameTooltip:SetHyperlink(itemLink)
GameTooltip:Show()
end,
onLeave = onLeave,
}
else
-- Scanned empty or wrong expansion gear; pending = item data still loading
local isPending = slot and slot.pending
icons[i] = {
iconFileID = EMPTY_SLOT_TEXTURE,
size = GEAR_ICON_SIZE,
onEnter = function(frame)
GameTooltip:SetOwner(frame, "ANCHOR_RIGHT")
GameTooltip:SetText(isPending and "Loading..." or "Empty", 1, 1, 1)
GameTooltip:Show()
end,
onLeave = onLeave,
}
end
end
return icons
end


function Main:Render()
local selectedExpansions = Data.db.global.main.selectedExpansions or {}
local expansions = Data:GetExpansions()
Expand Down Expand Up @@ -1039,6 +1106,39 @@ function Main:GetTableColumns(unfiltered)
end,
},
},
{
id = "gear",
headerText = "Gear",
width = 90,
align = "CENTER",
toggleHidden = true,
onEnter = function(cellFrame)
GameTooltip:SetOwner(cellFrame, "ANCHOR_RIGHT")
GameTooltip:SetText("Gear", 1, 1, 1)
GameTooltip:AddLine("Profession tool and accessories.", nil, nil, nil, true)
GameTooltip:AddLine(" ")
GameTooltip:AddLine("Sorted by gear score (0-45): each slot scores 1-5 for Uncommon, 6-10 for Rare, and 11-15 for Epic, based on item rank.", nil, nil, nil, true)
GameTooltip:Show()
end,
onLeave = function() GameTooltip:Hide() end,
renderCell = function(data)
return { icons = GearCellIcons(data.characterProfession, data.skillLineVariantID) }
end,
sorting = {
enabled = true,
compare = function(a, b)
local scoreA = a.data.characterProfession.profGearScore
local scoreB = b.data.characterProfession.profGearScore
if scoreA == nil and scoreB == nil then
return (a.data.character.lastUpdate or 0) < (b.data.character.lastUpdate or 0)
end
if scoreA == nil then return true end
if scoreB == nil then return false end
if scoreA ~= scoreB then return scoreA < scoreB end
return (a.data.character.lastUpdate or 0) < (b.data.character.lastUpdate or 0)
end,
},
},
}

-- Category Progress
Expand Down
83 changes: 78 additions & 5 deletions Table.lua
Original file line number Diff line number Diff line change
Expand Up @@ -353,11 +353,84 @@ function Table:CreateFrame(config)
if isHeaderRow and tableFrame.config.sorting and tableFrame.config.sorting.enabled then
columnFrame:RegisterForClicks("LeftButtonUp", "RightButtonUp")
end
columnFrame.text:SetWordWrap(false)
columnFrame.text:SetJustifyH(columnTextAlign)
columnFrame.text:SetPoint("TOPLEFT", columnFrame, "TOPLEFT", tableFrame.config.cells.padding, -tableFrame.config.cells.padding)
columnFrame.text:SetPoint("BOTTOMRIGHT", columnFrame, "BOTTOMRIGHT", -tableFrame.config.cells.padding, tableFrame.config.cells.padding)
columnFrame.text:SetText(column.text)
if column.icons then
-- Icon cell: hide the FontString, show/create icon child frames
columnFrame.text:Hide()
columnFrame.iconFrames = columnFrame.iconFrames or {}

for i, iconData in ipairs(column.icons) do
local iconFrame = columnFrame.iconFrames[i]
if not iconFrame then
iconFrame = CreateFrame("Frame", nil, columnFrame)
iconFrame.texture = iconFrame:CreateTexture(nil, "ARTWORK")
iconFrame.texture:SetPoint("TOPLEFT", iconFrame, "TOPLEFT", 1, -1)
iconFrame.texture:SetPoint("BOTTOMRIGHT", iconFrame, "BOTTOMRIGHT", -1, 1)
iconFrame.border = iconFrame:CreateTexture(nil, "BACKGROUND")
iconFrame.border:SetAllPoints()
iconFrame.overlay = iconFrame:CreateTexture(nil, "OVERLAY")
columnFrame.iconFrames[i] = iconFrame
end

local iconSize = iconData.size or 18
iconFrame:SetSize(iconSize, iconSize)
iconFrame.texture:SetTexture(iconData.iconFileID)
if iconData.unscanned then
iconFrame.texture:SetVertexColor(0.4, 0.4, 0.4, 1)
else
iconFrame.texture:SetVertexColor(1, 1, 1, 1)
end

if iconData.borderColor then
local bc = iconData.borderColor
iconFrame.border:SetColorTexture(bc.r, bc.g, bc.b, 1)
iconFrame.border:Show()
else
iconFrame.border:Hide()
end

iconFrame.overlay:SetSize(10, 10)
iconFrame.overlay:ClearAllPoints()
iconFrame.overlay:SetPoint("TOPLEFT", iconFrame, "TOPLEFT", -2, 2)
if iconData.overlayAtlas then
iconFrame.overlay:SetAtlas(iconData.overlayAtlas)
iconFrame.overlay:Show()
else
iconFrame.overlay:Hide()
end

iconFrame:ClearAllPoints()
iconFrame:SetPoint("LEFT", columnFrame, "LEFT",
tableFrame.config.cells.padding + (i - 1) * (iconSize + 2), 0)
if iconData.onEnter or iconData.onLeave then
iconFrame:EnableMouse(true)
iconFrame:SetScript("OnEnter", iconData.onEnter or nil)
iconFrame:SetScript("OnLeave", iconData.onLeave or nil)
iconFrame:SetScript("OnMouseUp", function() columnFrame:onClickHandler(columnFrame) end)
else
iconFrame:EnableMouse(false)
iconFrame:SetScript("OnEnter", nil)
iconFrame:SetScript("OnLeave", nil)
iconFrame:SetScript("OnMouseUp", nil)
end
iconFrame:Show()
end

-- Hide leftover icon frames from a previous wider render
for i = #column.icons + 1, #columnFrame.iconFrames do
columnFrame.iconFrames[i]:Hide()
end
else
-- Text cell: show the FontString, hide any icon child frames
columnFrame.text:Show()
columnFrame.text:SetWordWrap(false)
columnFrame.text:SetJustifyH(columnTextAlign)
columnFrame.text:SetPoint("TOPLEFT", columnFrame, "TOPLEFT", tableFrame.config.cells.padding, -tableFrame.config.cells.padding)
columnFrame.text:SetPoint("BOTTOMRIGHT", columnFrame, "BOTTOMRIGHT", -tableFrame.config.cells.padding, tableFrame.config.cells.padding)
columnFrame.text:SetText(column.text)
if columnFrame.iconFrames then
for _, f in ipairs(columnFrame.iconFrames) do f:Hide() end
end
end
columnFrame:Show()

if column.backgroundColor then
Expand Down
Loading