import { ActionRowBuilder, ButtonBuilder, ButtonStyle, AttachmentBuilder, Colors, ContainerBuilder, SectionBuilder, TextDisplayBuilder, MediaGalleryBuilder, MediaGalleryItemBuilder, ThumbnailBuilder, SeparatorBuilder, SeparatorSpacingSize, MessageFlags } from "discord.js"; import { resolveAssetUrl, isLocalAssetUrl } from "@shared/lib/assets"; import { join } from "path"; import { existsSync } from "fs"; import { LootType, EffectType } from "@shared/lib/constants"; import type { LootTableItem } from "@shared/lib/types"; // Rarity Color Map const RarityColors: Record = { "C": Colors.LightGrey, "R": Colors.Blue, "SR": Colors.Purple, "SSR": Colors.Gold, "CURRENCY": Colors.Green, "XP": Colors.Aqua, "NOTHING": Colors.DarkButNotBlack }; const TitleMap: Record = { "C": "📦 Common Items", "R": "📦 Rare Items", "SR": "✨ Super Rare Items", "SSR": "🌟 SSR Items", "CURRENCY": "💰 Currency", "XP": "🔮 Experience", "NOTHING": "💨 Empty" }; export function getShopListingMessage( item: { id: number; name: string; description: string | null; formattedPrice: string; iconUrl: string | null; imageUrl: string | null; price: number | bigint; usageData?: any; rarity?: string; }, context?: { referencedItems: Map } ) { const files: AttachmentBuilder[] = []; let thumbnailUrl = resolveAssetUrl(item.iconUrl); let displayImageUrl = resolveAssetUrl(item.imageUrl); // Handle local icon if (item.iconUrl && isLocalAssetUrl(item.iconUrl)) { const iconPath = join(process.cwd(), "bot/assets/graphics", item.iconUrl.replace(/^\/?assets\//, "")); if (existsSync(iconPath)) { const iconName = defaultName(item.iconUrl); files.push(new AttachmentBuilder(iconPath, { name: iconName })); thumbnailUrl = `attachment://${iconName}`; } } // Handle local image if (item.imageUrl && isLocalAssetUrl(item.imageUrl)) { if (item.imageUrl === item.iconUrl && thumbnailUrl?.startsWith("attachment://")) { displayImageUrl = thumbnailUrl; } else { const imagePath = join(process.cwd(), "bot/assets/graphics", item.imageUrl.replace(/^\/?assets\//, "")); if (existsSync(imagePath)) { const imageName = defaultName(item.imageUrl); if (!files.find(f => f.name === imageName)) { files.push(new AttachmentBuilder(imagePath, { name: imageName })); } displayImageUrl = `attachment://${imageName}`; } } } const containers: ContainerBuilder[] = []; // 1. Main Container const mainContainer = new ContainerBuilder() .setAccentColor(RarityColors[item.rarity || "C"] || Colors.Green); // Header Section const infoSection = new SectionBuilder() .addTextDisplayComponents( new TextDisplayBuilder().setContent(`# ${item.name}`), new TextDisplayBuilder().setContent(item.description || "_No description available._"), new TextDisplayBuilder().setContent(`### 🏷️ Price: ${item.formattedPrice}`) ); // Set Thumbnail Accessory if we have an icon if (thumbnailUrl) { infoSection.setThumbnailAccessory(new ThumbnailBuilder().setURL(thumbnailUrl)); } mainContainer.addSectionComponents(infoSection); // Media Gallery for additional images (if multiple) const mediaSources: string[] = []; if (thumbnailUrl) mediaSources.push(thumbnailUrl); if (displayImageUrl && displayImageUrl !== thumbnailUrl) mediaSources.push(displayImageUrl); if (mediaSources.length > 1) { mainContainer.addMediaGalleryComponents( new MediaGalleryBuilder().addItems( ...mediaSources.map(src => new MediaGalleryItemBuilder().setURL(src)) ) ); } // 2. Loot Table (if applicable) if (item.usageData?.effects?.find((e: any) => e.type === EffectType.LOOTBOX)) { const lootboxEffect = item.usageData.effects.find((e: any) => e.type === EffectType.LOOTBOX); const pool = lootboxEffect.pool as LootTableItem[]; const totalWeight = pool.reduce((sum, i) => sum + i.weight, 0); mainContainer.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small)); mainContainer.addTextDisplayComponents(new TextDisplayBuilder().setContent("## 🎁 Potential Rewards")); const groups: Record = {}; for (const drop of pool) { const chance = ((drop.weight / totalWeight) * 100).toFixed(1); let line = ""; let rarity = "C"; switch (drop.type as any) { case LootType.CURRENCY: const currAmount = (drop.minAmount != null && drop.maxAmount != null) ? `${drop.minAmount} - ${drop.maxAmount}` : (Array.isArray(drop.amount) ? `${drop.amount[0]} - ${drop.amount[1]}` : drop.amount || 0); line = `**${currAmount} 🪙** (${chance}%)`; rarity = "CURRENCY"; break; case LootType.XP: const xpAmount = (drop.minAmount != null && drop.maxAmount != null) ? `${drop.minAmount} - ${drop.maxAmount}` : (Array.isArray(drop.amount) ? `${drop.amount[0]} - ${drop.amount[1]}` : drop.amount || 0); line = `**${xpAmount} XP** (${chance}%)`; rarity = "XP"; break; case LootType.ITEM: const referencedItems = context?.referencedItems; if (drop.itemId && referencedItems?.has(drop.itemId)) { const i = referencedItems.get(drop.itemId)!; line = `**${i.name}** x${drop.amount || 1} (${chance}%)`; rarity = i.rarity; } else { line = `**Unknown Item** (${chance}%)`; rarity = "C"; } break; case LootType.NOTHING: line = `**Nothing** (${chance}%)`; rarity = "NOTHING"; break; } if (line) { if (!groups[rarity]) groups[rarity] = []; groups[rarity]!.push(line); } } const order = ["SSR", "SR", "R", "C", "CURRENCY", "XP", "NOTHING"]; for (const rarity of order) { if (groups[rarity] && groups[rarity]!.length > 0) { mainContainer.addTextDisplayComponents( new TextDisplayBuilder().setContent(`### ${TitleMap[rarity] || rarity}`), new TextDisplayBuilder().setContent(groups[rarity]!.join("\n")) ); } } } // Purchase Row const buyButton = new ButtonBuilder() .setCustomId(`shop_buy_${item.id}`) .setLabel(`Purchase for ${item.price} 🪙`) .setStyle(ButtonStyle.Success) .setEmoji("🛒"); mainContainer.addActionRowComponents( new ActionRowBuilder().addComponents(buyButton) ); containers.push(mainContainer); return { components: containers as any, files, flags: MessageFlags.IsComponentsV2 }; } function defaultName(path: string): string { return path.split("/").pop() || "image.png"; }