┌─ TS ───────────────────────────────────────────────────────────────────────┐
│ // shared equipment utilities for consistent rendering │
│ │
│ // import color functions from canonical WoW constants │
│ export { getClassColor, getClassColorClass } from "./wow-constants.ts"; │
│ │
│ export function getQualityColorClass(quality: string): string { │
│ const qualityMap = { │
│ POOR: "quality-poor", │
│ COMMON: "quality-common", │
│ UNCOMMON: "quality-uncommon", │
│ RARE: "quality-rare", │
│ EPIC: "quality-epic", │
│ LEGENDARY: "quality-legendary", │
│ ARTIFACT: "quality-artifact", │
│ HEIRLOOM: "quality-heirloom", │
│ }; │
│ return qualityMap[quality as keyof typeof qualityMap] || "quality-common"; │
│ } │
│ │
│ // equipment slot ordering for display │
│ export const EQUIPMENT_SLOT_ORDER = [ │
│ "HEAD", │
│ "NECK", │
│ "SHOULDER", │
│ "BACK", │
│ "CHEST", │
│ "WRIST", │
│ "HANDS", │
│ "WAIST", │
│ "LEGS", │
│ "FEET", │
│ "FINGER_1", │
│ "FINGER_2", │
│ "TRINKET_1", │
│ "TRINKET_2", │
│ "MAIN_HAND", │
│ "OFF_HAND", │
│ "RANGED", │
│ ] as const; │
│ │
│ // simulation slot names mapping (from chart-logic.js) │
│ export const SIMULATION_SLOT_NAMES = [ │
│ "Head", │
│ "Neck", │
│ "Shoulders", │
│ "Back", │
│ "Chest", │
│ "Wrists", │
│ "Hands", │
│ "Waist", │
│ "Legs", │
│ "Feet", │
│ "Ring 1", │
│ "Ring 2", │
│ "Trinket 1", │
│ "Trinket 2", │
│ "Main Hand", │
│ "Off Hand", │
│ ] as const; │
│ │
│ interface ItemData { │
│ slot?: string; │
│ slot_type?: string; │
│ item_id?: number; │
│ id?: number; │
│ item_name?: string; │
│ name?: string; │
│ quality?: string; │
│ upgrade_id?: number; │
│ stats?: { ilvl?: number }; │
│ enchantments?: Array<{ │
│ slot_id: number; │
│ slot_type?: string; │
│ display_string: string; │
│ gem_name?: string; │
│ gem_icon_slug?: string; │
│ }>; │
│ gems?: Array< │
│ | { │
│ name?: string; │
│ icon?: string; │
│ stats?: string[]; │
│ } │
│ | number │
│ >; │
│ enchant?: │
│ | { │
│ name?: string; │
│ } │
│ | number; │
│ reforging?: │
│ | { │
│ description?: string; │
│ } │
│ | number; │
│ tinker?: number; │
│ item_icon_slug?: string; │
│ icon?: string; │
│ itemDetails?: string[]; │
│ } │
│ │
│ interface CreateItemElementOptions { │
│ isHTML?: boolean; │
│ showIcon?: boolean; │
│ showSlotHeader?: boolean; │
│ iconSize?: "small" | "medium" | "large"; │
│ } │
│ │
│ // unified function to create equipment item element/HTML │
│ // handles both Blizzard API data and simulation data formats │
│ export function createItemElement( │
│ itemData: ItemData, │
│ options: CreateItemElementOptions = {}, │
│ ): string | HTMLElement { │
│ const { │
│ isHTML = false, │
│ showIcon = true, │
│ showSlotHeader = true, │
│ iconSize = "medium", │
│ } = options; │
│ │
│ // normalize slot name │
│ const slot = formatSlotName(itemData.slot || itemData.slot_type || ""); │
│ │
│ // item basics │
│ const itemId = itemData.item_id || itemData.id; │
│ const itemName = itemData.item_name || itemData.name || `Item ${itemId}`; │
│ const quality = itemData.quality || "COMMON"; │
│ const qualityClass = getQualityColorClass(quality); │
│ │
│ // build enhancement details │
│ const details = buildItemDetails(itemData); │
│ │
│ // icon handling (with fallback) │
│ const iconHtml = showIcon ? buildIconHtml(itemData, iconSize) : ""; │
│ │
│ const itemHtml = ` │
│ <div class="equipment-slot"> │
│ ${ │
│ showSlotHeader │
│ ? ` │
│ <div class="equipment-slot-header"> │
│ <span class="equipment-slot-name">${slot}</span> │
│ </div> │
│ ` │
│ : "" │
│ } │
│ │
│ <div class="equipment-item-tooltip"> │
│ <div class="equipment-item-header"> │
│ ${iconHtml} │
│ <div class="equipment-item-info"> │
│ <a │
│ href="${getWowheadUrl(itemId!)}" │
│ class="equipment-item-link ${qualityClass}" │
│ target="_blank" │
│ rel="noopener noreferrer" │
│ > │
│ ${itemName} │
│ </a> │
│ ${itemData.upgrade_id ? `<div class="item-level">Item Level: Base + ${ │
│ itemData.upgrade_id}</div>` : ""} │
│ ${itemData.stats?.ilvl ? `<div class="item-level">Item Level ${itemDat │
│ a.stats.ilvl}</div>` : ""} │
│ </div> │
│ </div> │
│ │
│ ${ │
│ details.length > 0 │
│ ? ` │
│ <div class="item-tooltip-details"> │
│ ${details.join("")} │
│ </div> │
│ ` │
│ : "" │
│ } │
│ </div> │
│ </div> │
│ `; │
│ │
│ if (isHTML) { │
│ return itemHtml; │
│ } │
│ │
│ // create DOM element (client-side only) │
│ if (typeof document !== "undefined") { │
│ const div = document.createElement("div"); │
│ div.innerHTML = itemHtml; │
│ return div.firstElementChild as HTMLElement; │
│ } │
│ │
│ // server-side fallback │
│ return itemHtml; │
│ } │
│ │
│ // build comprehensive item details (gems, enchants, reforging, tinkers) │
│ function buildItemDetails(itemData: ItemData): string[] { │
│ const details: string[] = []; │
│ │
│ // handle chart format pre-processed details │
│ if (itemData.itemDetails && Array.isArray(itemData.itemDetails)) { │
│ return itemData.itemDetails; │
│ } │
│ │
│ // handle Blizzard API enchantments format │
│ if (itemData.enchantments) { │
│ console.log( │
│ "Processing enchantments for item:", │
│ itemData.item_name, │
│ itemData.enchantments, │
│ ); │
│ itemData.enchantments.forEach((enchant) => { │
│ const isGem = │
│ enchant.slot_id >= 2 && enchant.slot_id <= 5 && !enchant.slot_type; │
│ const isEnchant = enchant.slot_type === "PERMANENT"; │
│ const isTinker = enchant.slot_type === "ON_USE_SPELL"; │
│ │
│ console.log( │
│ "Enchantment:", │
│ enchant, │
│ "isGem:", │
│ isGem, │
│ "gem_icon_slug:", │
│ enchant.gem_icon_slug, │
│ ); │
│ │
│ if (isGem) { │
│ // use gem icon from database if available │
│ const gemIcon = enchant.gem_icon_slug │
│ ? `<img src="${getZamimgIconUrl(enchant.gem_icon_slug, "small")}" alt="$ │
│ {enchant.gem_name || enchant.display_string}" class="gem-icon-inline" />` │
│ : ""; │
│ console.log("Building gem with icon:", gemIcon); │
│ details.push( │
│ `<div class="gem-line">${gemIcon}<span class="gem-stats-white">${enchant │
│ .display_string}</span></div>`, │
│ ); │
│ } else if (isEnchant || isTinker) { │
│ details.push( │
│ `<div class="${isEnchant ? "item-enchant" : "equipment-detail"}">${encha │
│ nt.display_string}</div>`, │
│ ); │
│ } else { │
│ details.push( │
│ `<div class="equipment-detail">${enchant.display_string}</div>`, │
│ ); │
│ } │
│ }); │
│ } │
│ │
│ // handle simulation format gems │
│ if (itemData.gems) { │
│ itemData.gems.forEach((gem) => { │
│ if (gem && typeof gem === "object" && gem.name) { │
│ const gemIcon = gem.icon │
│ ? `<img src="https://wow.zamimg.com/images/wow/icons/small/${gem.icon}.j │
│ pg" alt="${gem.name}" class="gem-icon-inline" />` │
│ : ""; │
│ const gemStats = gem.stats?.length ? gem.stats.join(", ") : gem.name; │
│ details.push( │
│ `<div class="gem-line">${gemIcon}<span class="gem-stats-white">${gemStat │
│ s}</span></div>`, │
│ ); │
│ } else if (gem && typeof gem === "number") { │
│ details.push( │
│ `<div class="gem-line"><span class="gem-stats-white">Gem ${gem}</span></ │
│ div>`, │
│ ); │
│ } │
│ }); │
│ } │
│ │
│ // handle simulation format enchant │
│ if (itemData.enchant) { │
│ const enchantText = │
│ typeof itemData.enchant === "object" && itemData.enchant.name │
│ ? itemData.enchant.name │
│ : `Enchant ${itemData.enchant}`; │
│ details.push(`<div class="item-enchant">${enchantText}</div>`); │
│ } │
│ │
│ // handle simulation format reforging │
│ if (itemData.reforging) { │
│ let reforgeText = ""; │
│ if ( │
│ typeof itemData.reforging === "object" && │
│ itemData.reforging.description │
│ ) { │
│ reforgeText = `Reforged ${itemData.reforging.description}`; │
│ } else if (typeof itemData.reforging === "number") { │
│ // use ItemDatabase if available for reforge lookup │
│ reforgeText = │
│ (window as any).ItemDatabase?.formatReforge?.(itemData.reforging) || │
│ `Reforge ${itemData.reforging}`; │
│ } │
│ if (reforgeText) { │
│ details.push(`<div class="item-reforge">${reforgeText}</div>`); │
│ } │
│ } │
│ │
│ // handle simulation format tinker │
│ if (itemData.tinker) { │
│ details.push( │
│ `<div class="equipment-detail">Tinker ${itemData.tinker}</div>`, │
│ ); │
│ } │
│ │
│ return details; │
│ } │
│ │
│ // build icon HTML with fallback support │
│ function buildIconHtml( │
│ itemData: ItemData, │
│ size: "small" | "medium" | "large" = "medium", │
│ ): string { │
│ const itemId = itemData.item_id || itemData.id; │
│ │
│ // use provided icon slug if available │
│ if (itemData.item_icon_slug || itemData.icon) { │
│ const iconSlug = itemData.item_icon_slug || itemData.icon!; │
│ return `<img class="equipment-icon" src="${getZamimgIconUrl(iconSlug, size)}" │
│ alt="${itemData.item_name || itemData.name || ""}" loading="lazy" />`; │
│ } │
│ │
│ // fallback: try item ID (won't work but placeholder for future) │
│ if (itemId) { │
│ return `<img class="equipment-icon" src="${getZamimgIconUrl(itemId.toString(), │
│ size)}" alt="${itemData.item_name || itemData.name || ""}" loading="lazy" onerror │
│ ="this.style.display='none'" />`; │
│ } │
│ │
│ return ""; │
│ } │
│ │
│ // format equipment from simulation data (extracted from chart-logic.js) │
│ export function formatEquipmentSummary(items: any[]): any[] { │
│ const equipment: any[] = []; │
│ items.forEach((item, index) => { │
│ if (item) { │
│ const id = item.id || item.itemId || item.item_id; │
│ if (!id) return; │
│ const itemInfo = (window as any).ItemDatabase?.formatEquipmentItem?.({ │
│ ...item, │
│ id, │
│ }) || { │
│ itemId: id, │
│ itemName: item.name || item.item_name || `Item ${id}`, │
│ itemDetails: [], │
│ wowheadUrl: getWowheadUrl(id), │
│ iconUrl: item.icon ? getZamimgIconUrl(item.icon, "large") : undefined, │
│ quality: item.quality || null, │
│ }; │
│ │
│ if (itemInfo) { │
│ equipment.push({ │
│ slot: SIMULATION_SLOT_NAMES[index] || `Slot ${index + 1}`, │
│ item_id: itemInfo.itemId, │
│ item_name: itemInfo.itemName, │
│ quality: itemInfo.quality, │
│ ...itemInfo, │
│ }); │
│ } │
│ } │
│ }); │
│ return equipment; │
│ } │
│ │
│ export function processEquipmentEnchantments(equipment: any[]): any[] { │
│ return equipment.map((item) => { │
│ if (!item.enchantments) return item; │
│ │
│ return { │
│ ...item, │
│ processedEnchantments: item.enchantments.map((enchant: any) => { │
│ const isGem = │
│ enchant.slot_id >= 2 && enchant.slot_id <= 5 && !enchant.slot_type; │
│ const isEnchant = enchant.slot_type === "PERMANENT"; │
│ const isTinker = enchant.slot_type === "ON_USE_SPELL"; │
│ │
│ return { │
│ ...enchant, │
│ isGem, │
│ isEnchant, │
│ isTinker, │
│ }; │
│ }), │
│ }; │
│ }); │
│ } │
│ │
│ export function getOrderedEquipment(equipment: Record<string, any>): any[] { │
│ return EQUIPMENT_SLOT_ORDER.map((slot) => equipment[slot]).filter( │
│ (item) => item != null, │
│ ); │
│ } │
│ │
│ export function formatSlotName(slotType: string): string { │
│ return slotType.replace("_", " "); │
│ } │
│ │
│ export function getWowheadUrl( │
│ itemId: number, │
│ expansion: string = "mop-classic", │
│ ): string { │
│ return `https://www.wowhead.com/${expansion}/item=${itemId}`; │
│ } │
│ │
│ export function getZamimgIconUrl( │
│ itemId: string | number, │
│ size: "small" | "medium" | "large" = "medium", │
│ ): string { │
│ return `https://wow.zamimg.com/images/wow/icons/${size}/${itemId}.jpg`; │
│ } │
│ │
│ // Compact equipment icon grid (used by loadout dropdowns) │
│ export function renderEquipmentIconsCompact(items: any[]): string { │
│ if (!Array.isArray(items) || items.length === 0) return ""; │
│ │
│ const toQualityClass = (q: any) => { │
│ const map: Record<string, string> = { │
│ "0": "quality-poor", │
│ "1": "quality-common", │
│ "2": "quality-uncommon", │
│ "3": "quality-rare", │
│ "4": "quality-epic", │
│ "5": "quality-legendary", │
│ "6": "quality-artifact", │
│ "7": "quality-heirloom", │
│ }; │
│ if (typeof q === "number") return map[String(q)] || "quality-common"; │
│ if (typeof q === "string") { │
│ const s = q.toLowerCase(); │
│ if (s.startsWith("quality-")) return s; │
│ return map[s] || `quality-${s}`; │
│ } │
│ return "quality-common"; │
│ }; │
│ │
│ const cells = items │
│ .map((it: any) => { │
│ const id = it.id || it.item_id || it.itemId; │
│ if (!id) return ""; │
│ const href = getWowheadUrl(id); │
│ const title = it.name || it.item_name || `Item ${id}`; │
│ const qClass = toQualityClass(it.quality); │
│ const iconSlug = it.icon || it.item_icon_slug || null; │
│ const img = iconSlug │
│ ? iconSlug.startsWith?.("http") │
│ ? iconSlug │
│ : getZamimgIconUrl(iconSlug, "large") │
│ : it.iconUrl || ""; │
│ │
│ let badgeHtml = ""; │
│ if (Array.isArray(it.enchantments)) { │
│ const egems = it.enchantments.filter((e: any) => e && e.gem_icon_slug); │
│ if (egems.length) { │
│ badgeHtml = `<div class=\"gem-stack\">${egems │
│ .slice(0, 3) │
│ .map( │
│ (g: any) => │
│ `<img class=\"gem-badge\" src=\"${getZamimgIconUrl(g.gem_icon_slug │
│ , "small")}\" alt=\"${g.gem_name || "Gem"}\" title=\"${g.gem_name || g.display_str │
│ ing || "Gem"}\" />`, │
│ ) │
│ .join("")}</div>`; │
│ } │
│ } else if (Array.isArray(it.gems)) { │
│ const ggems = it.gems.filter( │
│ (g: any) => g && typeof g === "object" && g.icon, │
│ ); │
│ if (ggems.length) { │
│ badgeHtml = `<div class=\"gem-stack\">${ggems │
│ .slice(0, 3) │
│ .map( │
│ (g: any) => │
│ `<img class=\"gem-badge\" src=\"${getZamimgIconUrl(g.icon, "small" │
│ )}\" alt=\"${g.name || "Gem"}\" title=\"${(g.stats && g.stats.join(", ")) || g.nam │
│ e || "Gem"}\" />`, │
│ ) │
│ .join("")}</div>`; │
│ } │
│ } │
│ │
│ return ` │
│ <a href="${href}" class="equip-icon-wrap ${qClass}" title="${title}" targe │
│ t="_blank" rel="noopener noreferrer"> │
│ <div class="equip-icon-inner"> │
│ ${img ? `<img class="equip-icon" src="${img}" alt="${title}" loading=" │
│ lazy" />` : `<div class="equip-icon placeholder" aria-label="${title}"></div>`} │
│ ${badgeHtml} │
│ </div> │
│ </a>`; │
│ }) │
│ .join(""); │
│ │
│ return `<div class=\"equipment-compact\"><div class=\"equipment-icon-grid\">${ce │
│ lls}</div></div>`; │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ TS ─────────────────────────────────┐
│ // shared equipment utilities for consistent │
│ rendering │
│ │
│ // import color functions from canonical WoW │
│ constants │
│ export { getClassColor, getClassColorClass } │
│ from "./wow-constants.ts"; │
│ │
│ export function getQualityColorClass(quality │
│ : string): string { │
│ const qualityMap = { │
│ POOR: "quality-poor", │
│ COMMON: "quality-common", │
│ UNCOMMON: "quality-uncommon", │
│ RARE: "quality-rare", │
│ EPIC: "quality-epic", │
│ LEGENDARY: "quality-legendary", │
│ ARTIFACT: "quality-artifact", │
│ HEIRLOOM: "quality-heirloom", │
│ }; │
│ return qualityMap[quality as keyof typeof │
│ qualityMap] || "quality-common"; │
│ } │
│ │
│ // equipment slot ordering for display │
│ export const EQUIPMENT_SLOT_ORDER = [ │
│ "HEAD", │
│ "NECK", │
│ "SHOULDER", │
│ "BACK", │
│ "CHEST", │
│ "WRIST", │
│ "HANDS", │
│ "WAIST", │
│ "LEGS", │
│ "FEET", │
│ "FINGER_1", │
│ "FINGER_2", │
│ "TRINKET_1", │
│ "TRINKET_2", │
│ "MAIN_HAND", │
│ "OFF_HAND", │
│ "RANGED", │
│ ] as const; │
│ │
│ // simulation slot names mapping (from chart │
│ -logic.js) │
│ export const SIMULATION_SLOT_NAMES = [ │
│ "Head", │
│ "Neck", │
│ "Shoulders", │
│ "Back", │
│ "Chest", │
│ "Wrists", │
│ "Hands", │
│ "Waist", │
│ "Legs", │
│ "Feet", │
│ "Ring 1", │
│ "Ring 2", │
│ "Trinket 1", │
│ "Trinket 2", │
│ "Main Hand", │
│ "Off Hand", │
│ ] as const; │
│ │
│ interface ItemData { │
│ slot?: string; │
│ slot_type?: string; │
│ item_id?: number; │
│ id?: number; │
│ item_name?: string; │
│ name?: string; │
│ quality?: string; │
│ upgrade_id?: number; │
│ stats?: { ilvl?: number }; │
│ enchantments?: Array<{ │
│ slot_id: number; │
│ slot_type?: string; │
│ display_string: string; │
│ gem_name?: string; │
│ gem_icon_slug?: string; │
│ }>; │
│ gems?: Array< │
│ | { │
│ name?: string; │
│ icon?: string; │
│ stats?: string[]; │
│ } │
│ | number │
│ >; │
│ enchant?: │
│ | { │
│ name?: string; │
│ } │
│ | number; │
│ reforging?: │
│ | { │
│ description?: string; │
│ } │
│ | number; │
│ tinker?: number; │
│ item_icon_slug?: string; │
│ icon?: string; │
│ itemDetails?: string[]; │
│ } │
│ │
│ interface CreateItemElementOptions { │
│ isHTML?: boolean; │
│ showIcon?: boolean; │
│ showSlotHeader?: boolean; │
│ iconSize?: "small" | "medium" | "large"; │
│ } │
│ │
│ // unified function to create equipment item │
│ element/HTML │
│ // handles both Blizzard API data and simula │
│ tion data formats │
│ export function createItemElement( │
│ itemData: ItemData, │
│ options: CreateItemElementOptions = {}, │
│ ): string | HTMLElement { │
│ const { │
│ isHTML = false, │
│ showIcon = true, │
│ showSlotHeader = true, │
│ iconSize = "medium", │
│ } = options; │
│ │
│ // normalize slot name │
│ const slot = formatSlotName(itemData.slot │
│ || itemData.slot_type || ""); │
│ │
│ // item basics │
│ const itemId = itemData.item_id || itemDat │
│ a.id; │
│ const itemName = itemData.item_name || ite │
│ mData.name || `Item ${itemId}`; │
│ const quality = itemData.quality || "COMMO │
│ N"; │
│ const qualityClass = getQualityColorClass( │
│ quality); │
│ │
│ // build enhancement details │
│ const details = buildItemDetails(itemData) │
│ ; │
│ │
│ // icon handling (with fallback) │
│ const iconHtml = showIcon ? buildIconHtml( │
│ itemData, iconSize) : ""; │
│ │
│ const itemHtml = ` │
│ <div class="equipment-slot"> │
│ ${ │
│ showSlotHeader │
│ ? ` │
│ <div class="equipment-slot-header"> │
│ <span class="equipment-slot-name"> │
│ ${slot}</span> │
│ </div> │
│ ` │
│ : "" │
│ } │
│ │
│ <div class="equipment-item-tooltip"> │
│ <div class="equipment-item-header"> │
│ ${iconHtml} │
│ <div class="equipment-item-info"> │
│ <a │
│ href="${getWowheadUrl(itemId!) │
│ }" │
│ class="equipment-item-link ${q │
│ ualityClass}" │
│ target="_blank" │
│ rel="noopener noreferrer" │
│ > │
│ ${itemName} │
│ </a> │
│ ${itemData.upgrade_id ? `<div cl │
│ ass="item-level">Item Level: Base + ${itemDa │
│ ta.upgrade_id}</div>` : ""} │
│ ${itemData.stats?.ilvl ? `<div c │
│ lass="item-level">Item Level ${itemData.stat │
│ s.ilvl}</div>` : ""} │
│ </div> │
│ </div> │
│ │
│ ${ │
│ details.length > 0 │
│ ? ` │
│ <div class="item-tooltip-details"> │
│ ${details.join("")} │
│ </div> │
│ ` │
│ : "" │
│ } │
│ </div> │
│ </div> │
│ `; │
│ │
│ if (isHTML) { │
│ return itemHtml; │
│ } │
│ │
│ // create DOM element (client-side only) │
│ if (typeof document !== "undefined") { │
│ const div = document.createElement("div" │
│ ); │
│ div.innerHTML = itemHtml; │
│ return div.firstElementChild as HTMLElem │
│ ent; │
│ } │
│ │
│ // server-side fallback │
│ return itemHtml; │
│ } │
│ │
│ // build comprehensive item details (gems, e │
│ nchants, reforging, tinkers) │
│ function buildItemDetails(itemData: ItemData │
│ ): string[] { │
│ const details: string[] = []; │
│ │
│ // handle chart format pre-processed detai │
│ ls │
│ if (itemData.itemDetails && Array.isArray( │
│ itemData.itemDetails)) { │
│ return itemData.itemDetails; │
│ } │
│ │
│ // handle Blizzard API enchantments format │
│ if (itemData.enchantments) { │
│ console.log( │
│ "Processing enchantments for item:", │
│ itemData.item_name, │
│ itemData.enchantments, │
│ ); │
│ itemData.enchantments.forEach((enchant) │
│ => { │
│ const isGem = │
│ enchant.slot_id >= 2 && enchant.slot │
│ _id <= 5 && !enchant.slot_type; │
│ const isEnchant = enchant.slot_type == │
│ = "PERMANENT"; │
│ const isTinker = enchant.slot_type === │
│ "ON_USE_SPELL"; │
│ │
│ console.log( │
│ "Enchantment:", │
│ enchant, │
│ "isGem:", │
│ isGem, │
│ "gem_icon_slug:", │
│ enchant.gem_icon_slug, │
│ ); │
│ │
│ if (isGem) { │
│ // use gem icon from database if ava │
│ ilable │
│ const gemIcon = enchant.gem_icon_slu │
│ g │
│ ? `<img src="${getZamimgIconUrl(en │
│ chant.gem_icon_slug, "small")}" alt="${encha │
│ nt.gem_name || enchant.display_string}" clas │
│ s="gem-icon-inline" />` │
│ : ""; │
│ console.log("Building gem with icon: │
│ ", gemIcon); │
│ details.push( │
│ `<div class="gem-line">${gemIcon}< │
│ span class="gem-stats-white">${enchant.displ │
│ ay_string}</span></div>`, │
│ ); │
│ } else if (isEnchant || isTinker) { │
│ details.push( │
│ `<div class="${isEnchant ? "item-e │
│ nchant" : "equipment-detail"}">${enchant.dis │
│ play_string}</div>`, │
│ ); │
│ } else { │
│ details.push( │
│ `<div class="equipment-detail">${e │
│ nchant.display_string}</div>`, │
│ ); │
│ } │
│ }); │
│ } │
│ │
│ // handle simulation format gems │
│ if (itemData.gems) { │
│ itemData.gems.forEach((gem) => { │
│ if (gem && typeof gem === "object" && │
│ gem.name) { │
│ const gemIcon = gem.icon │
│ ? `<img src="https://wow.zamimg.co │
│ m/images/wow/icons/small/${gem.icon}.jpg" al │
│ t="${gem.name}" class="gem-icon-inline" />` │
│ : ""; │
│ const gemStats = gem.stats?.length ? │
│ gem.stats.join(", ") : gem.name; │
│ details.push( │
│ `<div class="gem-line">${gemIcon}< │
│ span class="gem-stats-white">${gemStats}</sp │
│ an></div>`, │
│ ); │
│ } else if (gem && typeof gem === "numb │
│ er") { │
│ details.push( │
│ `<div class="gem-line"><span class │
│ ="gem-stats-white">Gem ${gem}</span></div>`, │
│ ); │
│ } │
│ }); │
│ } │
│ │
│ // handle simulation format enchant │
│ if (itemData.enchant) { │
│ const enchantText = │
│ typeof itemData.enchant === "object" & │
│ & itemData.enchant.name │
│ ? itemData.enchant.name │
│ : `Enchant ${itemData.enchant}`; │
│ details.push(`<div class="item-enchant"> │
│ ${enchantText}</div>`); │
│ } │
│ │
│ // handle simulation format reforging │
│ if (itemData.reforging) { │
│ let reforgeText = ""; │
│ if ( │
│ typeof itemData.reforging === "object" │
│ && │
│ itemData.reforging.description │
│ ) { │
│ reforgeText = `Reforged ${itemData.ref │
│ orging.description}`; │
│ } else if (typeof itemData.reforging === │
│ "number") { │
│ // use ItemDatabase if available for r │
│ eforge lookup │
│ reforgeText = │
│ (window as any).ItemDatabase?.format │
│ Reforge?.(itemData.reforging) || │
│ `Reforge ${itemData.reforging}`; │
│ } │
│ if (reforgeText) { │
│ details.push(`<div class="item-reforge │
│ ">${reforgeText}</div>`); │
│ } │
│ } │
│ │
│ // handle simulation format tinker │
│ if (itemData.tinker) { │
│ details.push( │
│ `<div class="equipment-detail">Tinker │
│ ${itemData.tinker}</div>`, │
│ ); │
│ } │
│ │
│ return details; │
│ } │
│ │
│ // build icon HTML with fallback support │
│ function buildIconHtml( │
│ itemData: ItemData, │
│ size: "small" | "medium" | "large" = "medi │
│ um", │
│ ): string { │
│ const itemId = itemData.item_id || itemDat │
│ a.id; │
│ │
│ // use provided icon slug if available │
│ if (itemData.item_icon_slug || itemData.ic │
│ on) { │
│ const iconSlug = itemData.item_icon_slug │
│ || itemData.icon!; │
│ return `<img class="equipment-icon" src= │
│ "${getZamimgIconUrl(iconSlug, size)}" alt="$ │
│ {itemData.item_name || itemData.name || ""}" │
│ loading="lazy" />`; │
│ } │
│ │
│ // fallback: try item ID (won't work but p │
│ laceholder for future) │
│ if (itemId) { │
│ return `<img class="equipment-icon" src= │
│ "${getZamimgIconUrl(itemId.toString(), size) │
│ }" alt="${itemData.item_name || itemData.nam │
│ e || ""}" loading="lazy" onerror="this.style │
│ .display='none'" />`; │
│ } │
│ │
│ return ""; │
│ } │
│ │
│ // format equipment from simulation data (ex │
│ tracted from chart-logic.js) │
│ export function formatEquipmentSummary(items │
│ : any[]): any[] { │
│ const equipment: any[] = []; │
│ items.forEach((item, index) => { │
│ if (item) { │
│ const id = item.id || item.itemId || i │
│ tem.item_id; │
│ if (!id) return; │
│ const itemInfo = (window as any).ItemD │
│ atabase?.formatEquipmentItem?.({ │
│ ...item, │
│ id, │
│ }) || { │
│ itemId: id, │
│ itemName: item.name || item.item_nam │
│ e || `Item ${id}`, │
│ itemDetails: [], │
│ wowheadUrl: getWowheadUrl(id), │
│ iconUrl: item.icon ? getZamimgIconUr │
│ l(item.icon, "large") : undefined, │
│ quality: item.quality || null, │
│ }; │
│ │
│ if (itemInfo) { │
│ equipment.push({ │
│ slot: SIMULATION_SLOT_NAMES[index] │
│ || `Slot ${index + 1}`, │
│ item_id: itemInfo.itemId, │
│ item_name: itemInfo.itemName, │
│ quality: itemInfo.quality, │
│ ...itemInfo, │
│ }); │
│ } │
│ } │
│ }); │
│ return equipment; │
│ } │
│ │
│ export function processEquipmentEnchantments │
│ (equipment: any[]): any[] { │
│ return equipment.map((item) => { │
│ if (!item.enchantments) return item; │
│ │
│ return { │
│ ...item, │
│ processedEnchantments: item.enchantmen │
│ ts.map((enchant: any) => { │
│ const isGem = │
│ enchant.slot_id >= 2 && enchant.sl │
│ ot_id <= 5 && !enchant.slot_type; │
│ const isEnchant = enchant.slot_type │
│ === "PERMANENT"; │
│ const isTinker = enchant.slot_type = │
│ == "ON_USE_SPELL"; │
│ │
│ return { │
│ ...enchant, │
│ isGem, │
│ isEnchant, │
│ isTinker, │
│ }; │
│ }), │
│ }; │
│ }); │
│ } │
│ │
│ export function getOrderedEquipment(equipmen │
│ t: Record<string, any>): any[] { │
│ return EQUIPMENT_SLOT_ORDER.map((slot) => │
│ equipment[slot]).filter( │
│ (item) => item != null, │
│ ); │
│ } │
│ │
│ export function formatSlotName(slotType: str │
│ ing): string { │
│ return slotType.replace("_", " "); │
│ } │
│ │
│ export function getWowheadUrl( │
│ itemId: number, │
│ expansion: string = "mop-classic", │
│ ): string { │
│ return `https://www.wowhead.com/${expansio │
│ n}/item=${itemId}`; │
│ } │
│ │
│ export function getZamimgIconUrl( │
│ itemId: string | number, │
│ size: "small" | "medium" | "large" = "medi │
│ um", │
│ ): string { │
│ return `https://wow.zamimg.com/images/wow/ │
│ icons/${size}/${itemId}.jpg`; │
│ } │
│ │
│ // Compact equipment icon grid (used by load │
│ out dropdowns) │
│ export function renderEquipmentIconsCompact( │
│ items: any[]): string { │
│ if (!Array.isArray(items) || items.length │
│ === 0) return ""; │
│ │
│ const toQualityClass = (q: any) => { │
│ const map: Record<string, string> = { │
│ "0": "quality-poor", │
│ "1": "quality-common", │
│ "2": "quality-uncommon", │
│ "3": "quality-rare", │
│ "4": "quality-epic", │
│ "5": "quality-legendary", │
│ "6": "quality-artifact", │
│ "7": "quality-heirloom", │
│ }; │
│ if (typeof q === "number") return map[St │
│ ring(q)] || "quality-common"; │
│ if (typeof q === "string") { │
│ const s = q.toLowerCase(); │
│ if (s.startsWith("quality-")) return s │
│ ; │
│ return map[s] || `quality-${s}`; │
│ } │
│ return "quality-common"; │
│ }; │
│ │
│ const cells = items │
│ .map((it: any) => { │
│ const id = it.id || it.item_id || it.i │
│ temId; │
│ if (!id) return ""; │
│ const href = getWowheadUrl(id); │
│ const title = it.name || it.item_name │
│ || `Item ${id}`; │
│ const qClass = toQualityClass(it.quali │
│ ty); │
│ const iconSlug = it.icon || it.item_ic │
│ on_slug || null; │
│ const img = iconSlug │
│ ? iconSlug.startsWith?.("http") │
│ ? iconSlug │
│ : getZamimgIconUrl(iconSlug, "larg │
│ e") │
│ : it.iconUrl || ""; │
│ │
│ let badgeHtml = ""; │
│ if (Array.isArray(it.enchantments)) { │
│ const egems = it.enchantments.filter │
│ ((e: any) => e && e.gem_icon_slug); │
│ if (egems.length) { │
│ badgeHtml = `<div class=\"gem-stac │
│ k\">${egems │
│ .slice(0, 3) │
│ .map( │
│ (g: any) => │
│ `<img class=\"gem-badge\" sr │
│ c=\"${getZamimgIconUrl(g.gem_icon_slug, "sma │
│ ll")}\" alt=\"${g.gem_name || "Gem"}\" title │
│ =\"${g.gem_name || g.display_string || "Gem" │
│ }\" />`, │
│ ) │
│ .join("")}</div>`; │
│ } │
│ } else if (Array.isArray(it.gems)) { │
│ const ggems = it.gems.filter( │
│ (g: any) => g && typeof g === "obj │
│ ect" && g.icon, │
│ ); │
│ if (ggems.length) { │
│ badgeHtml = `<div class=\"gem-stac │
│ k\">${ggems │
│ .slice(0, 3) │
│ .map( │
│ (g: any) => │
│ `<img class=\"gem-badge\" sr │
│ c=\"${getZamimgIconUrl(g.icon, "small")}\" a │
│ lt=\"${g.name || "Gem"}\" title=\"${(g.stats │
│ && g.stats.join(", ")) || g.name || "Gem"}\ │
│ " />`, │
│ ) │
│ .join("")}</div>`; │
│ } │
│ } │
│ │
│ return ` │
│ <a href="${href}" class="equip-icon- │
│ wrap ${qClass}" title="${title}" target="_bl │
│ ank" rel="noopener noreferrer"> │
│ <div class="equip-icon-inner"> │
│ ${img ? `<img class="equip-icon" │
│ src="${img}" alt="${title}" loading="lazy" │
│ />` : `<div class="equip-icon placeholder" a │
│ ria-label="${title}"></div>`} │
│ ${badgeHtml} │
│ </div> │
│ </a>`; │
│ }) │
│ .join(""); │
│ │
│ return `<div class=\"equipment-compact\">< │
│ div class=\"equipment-icon-grid\">${cells}</ │
│ div></div>`; │
│ } │
└──────────────────────────────────────────────┘