import { EmbedBuilder, AttachmentBuilder, ContainerBuilder, SectionBuilder, TextDisplayBuilder, MediaGalleryBuilder, MediaGalleryItemBuilder, ThumbnailBuilder, SeparatorBuilder, SeparatorSpacingSize, MessageFlags, } from "discord.js"; import { resolveAssetUrl, isLocalAssetUrl } from "@shared/lib/assets"; import { getRarityConfig, defaultName } from "@shared/lib/rarity"; import { join } from "path"; import { existsSync } from "fs"; /** * Inventory entry with item details */ interface InventoryEntry { quantity: bigint | null; item: { id: number; name: string; [key: string]: any; }; } /** * Creates an embed displaying a user's inventory */ export function getInventoryEmbed(items: InventoryEntry[], username: string): EmbedBuilder { const description = items.map(entry => { return `**${entry.item.name}** x${entry.quantity}`; }).join("\n"); return new EmbedBuilder() .setTitle(`📦 ${username}'s Inventory`) .setDescription(description) .setColor(0x3498db); // Blue } /** * Creates a Components V2 message showing the result of opening a lootbox. * Falls back to a simple embed for non-lootbox item usage. */ export function getLootboxResultMessage( results: any[], item?: { name: string; iconUrl: string | null; imageUrl: string | null; usageData: any } ) { const files: AttachmentBuilder[] = []; const otherMessages: string[] = []; let lootResult: any = null; for (const res of results) { if (typeof res === "object" && res.type === "LOOTBOX_RESULT") { lootResult = res; } else { otherMessages.push(typeof res === "string" ? `• ${res}` : `• ${JSON.stringify(res)}`); } } // If no loot result, fall back to a simple embed (non-lootbox item usage) if (!lootResult) { const embed = new EmbedBuilder() .setTitle(item ? `✅ Used ${item.name}` : "✅ Item Used!") .setDescription(otherMessages.join("\n") || "Effect applied.") .setColor(0x2ecc71) .setTimestamp(); return { embeds: [embed], files, components: undefined, flags: undefined }; } // Determine rarity key for theming let rarityKey = "C"; if (lootResult.rewardType === "ITEM" && lootResult.item) { rarityKey = lootResult.item.rarity || "C"; } else if (lootResult.rewardType === "CURRENCY") { rarityKey = "CURRENCY"; } else if (lootResult.rewardType === "XP") { rarityKey = "XP"; } else { rarityKey = "NOTHING"; } const config = getRarityConfig(rarityKey); const container = new ContainerBuilder().setAccentColor(config.color); // Header: lootbox name if (item?.name) { container.addTextDisplayComponents( new TextDisplayBuilder().setContent(`-# Opened: ${item.name}`) ); } // Build title and description based on reward type let title = ""; let description = ""; if (lootResult.rewardType === "ITEM" && lootResult.item) { const i = lootResult.item; const amountStr = lootResult.amount > 1 ? ` ×${lootResult.amount}` : ""; title = `${config.emoji} ${config.label} — ${i.name}${amountStr}`; description = i.description || ""; description += (description ? "\n\n" : "") + `**${config.label}** · ×${lootResult.amount || 1} added to inventory`; } else if (lootResult.rewardType === "CURRENCY") { title = `${config.emoji} You found ${lootResult.amount.toLocaleString()} AU!`; description = "Coins have been added to your balance."; } else if (lootResult.rewardType === "XP") { title = `${config.emoji} You gained ${lootResult.amount.toLocaleString()} XP!`; description = "Experience has been added to your profile."; } else { title = `${config.emoji} Empty...`; description = lootResult.message || "You found nothing inside."; } // Main section with optional thumbnail const section = new SectionBuilder().addTextDisplayComponents( new TextDisplayBuilder().setContent(`# ${title}`), new TextDisplayBuilder().setContent(description) ); // Thumbnail from iconUrl (use reward item's icon for ITEM, lootbox icon otherwise) let thumbnailUrl: string | null = null; const iconSource = lootResult.rewardType === "ITEM" ? lootResult.item?.iconUrl : item?.iconUrl; if (iconSource) { if (isLocalAssetUrl(iconSource)) { const iconPath = join(process.cwd(), "bot/assets/graphics", iconSource.replace(/^\/?assets\//, "")); if (existsSync(iconPath)) { const iconName = defaultName(iconSource); files.push(new AttachmentBuilder(iconPath, { name: iconName })); thumbnailUrl = `attachment://${iconName}`; } } else { thumbnailUrl = resolveAssetUrl(iconSource); } } if (thumbnailUrl) { section.setThumbnailAccessory(new ThumbnailBuilder().setURL(thumbnailUrl)); } container.addSectionComponents(section); // Media gallery for full item art (if imageUrl differs from iconUrl) if (lootResult.rewardType === "ITEM" && lootResult.item) { const imgSource = lootResult.item.imageUrl; const iconSrc = lootResult.item.iconUrl; if (imgSource && imgSource !== iconSrc) { let displayImageUrl: string | null = null; if (isLocalAssetUrl(imgSource)) { const imagePath = join(process.cwd(), "bot/assets/graphics", imgSource.replace(/^\/?assets\//, "")); if (existsSync(imagePath)) { const imageName = defaultName(imgSource); if (!files.find(f => f.name === imageName)) { files.push(new AttachmentBuilder(imagePath, { name: imageName })); } displayImageUrl = `attachment://${imageName}`; } } else { displayImageUrl = resolveAssetUrl(imgSource); } if (displayImageUrl) { container.addMediaGalleryComponents( new MediaGalleryBuilder().addItems( new MediaGalleryItemBuilder().setURL(displayImageUrl) ) ); } } } // Other effects (non-lootbox results like temp roles, XP boosts) if (otherMessages.length > 0) { container.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small)); container.addTextDisplayComponents( new TextDisplayBuilder().setContent(`**Other Effects**\n${otherMessages.join("\n")}`) ); } return { // TODO: remove cast once discord.js types include ContainerBuilder in MessageEditOptions components: [container] as any, files, flags: MessageFlags.IsComponentsV2, embeds: undefined, }; }