diff --git a/bot/commands/inventory/use.ts b/bot/commands/inventory/use.ts index 017e2d0..baa1d96 100644 --- a/bot/commands/inventory/use.ts +++ b/bot/commands/inventory/use.ts @@ -3,7 +3,7 @@ import { SlashCommandBuilder } from "discord.js"; import { inventoryService } from "@shared/modules/inventory/inventory.service"; import { userService } from "@shared/modules/user/user.service"; import { createErrorEmbed } from "@lib/embeds"; -import { getItemUseResultEmbed } from "@/modules/inventory/inventory.view"; +import { getLootboxResultMessage } from "@/modules/inventory/inventory.view"; import { withCommandErrorHandling } from "@lib/commandUtils"; import { getGuildConfig } from "@shared/lib/config"; @@ -57,9 +57,8 @@ export const use = createCommand({ } } - const { embed, files } = getItemUseResultEmbed(result.results, result.item); - - await interaction.editReply({ embeds: [embed], files }); + const message = getLootboxResultMessage(result.results, result.item); + await interaction.editReply(message as any); } ); }, diff --git a/bot/modules/inventory/inventory.view.ts b/bot/modules/inventory/inventory.view.ts index bdb9f24..7201382 100644 --- a/bot/modules/inventory/inventory.view.ts +++ b/bot/modules/inventory/inventory.view.ts @@ -1,7 +1,18 @@ -import { EmbedBuilder, AttachmentBuilder } from "discord.js"; -import type { ItemUsageData } from "@shared/lib/types"; -import { EffectType } from "@shared/lib/constants"; +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"; @@ -32,109 +43,147 @@ export function getInventoryEmbed(items: InventoryEntry[], username: string): Em } /** - * Creates an embed showing the results of using an item + * 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 getItemUseResultEmbed(results: any[], item?: { name: string, iconUrl: string | null, usageData: any }): { embed: EmbedBuilder, files: AttachmentBuilder[] } { - const embed = new EmbedBuilder(); +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') { + if (typeof res === "object" && res.type === "LOOTBOX_RESULT") { lootResult = res; } else { - otherMessages.push(typeof res === 'string' ? `โ€ข ${res}` : `โ€ข ${JSON.stringify(res)}`); + otherMessages.push(typeof res === "string" ? `โ€ข ${res}` : `โ€ข ${JSON.stringify(res)}`); } } - // Default Configuration - const isLootbox = item?.usageData?.effects?.some((e: any) => e.type === EffectType.LOOTBOX); - embed.setColor(isLootbox ? 0xFFD700 : 0x2ecc71); // Gold for lootbox, Green otherwise by default - embed.setTimestamp(); - - if (lootResult) { - embed.setTitle(`๐ŸŽ ${item?.name || "Lootbox"} Opened!`); - - if (lootResult.rewardType === 'ITEM' && lootResult.item) { - const i = lootResult.item; - const amountStr = lootResult.amount > 1 ? `x${lootResult.amount}` : ''; - - // Rarity Colors - const rarityColors: Record = { - 'C': 0x95A5A6, // Gray - 'R': 0x3498DB, // Blue - 'SR': 0x9B59B6, // Purple - 'SSR': 0xF1C40F // Gold - }; - - const rarityKey = i.rarity || 'C'; - if (rarityKey in rarityColors) { - embed.setColor(rarityColors[rarityKey] ?? 0x95A5A6); - } else { - embed.setColor(0x95A5A6); - } - - if (i.image) { - if (isLocalAssetUrl(i.image)) { - const imagePath = join(process.cwd(), "bot/assets/graphics", i.image.replace(/^\/?assets\//, "")); - if (existsSync(imagePath)) { - const imageName = defaultName(i.image); - if (!files.find(f => f.name === imageName)) { - files.push(new AttachmentBuilder(imagePath, { name: imageName })); - } - embed.setImage(`attachment://${imageName}`); - } - } else { - const imgUrl = resolveAssetUrl(i.image); - if (imgUrl) embed.setImage(imgUrl); - } - } - - embed.setDescription(`**You found ${i.name} ${amountStr}!**\n${i.description || '_'}`); - embed.addFields({ name: 'Rarity', value: rarityKey, inline: true }); - - } else if (lootResult.rewardType === 'CURRENCY') { - embed.setColor(0xF1C40F); - embed.setDescription(`**You found ${lootResult.amount.toLocaleString()} ๐Ÿช™ AU!**`); - } else if (lootResult.rewardType === 'XP') { - embed.setColor(0x2ECC71); // Green - embed.setDescription(`**You gained ${lootResult.amount.toLocaleString()} XP!**`); - } else { - // Nothing or Message - embed.setDescription(lootResult.message); - embed.setColor(0x95A5A6); // Gray - } + // 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 { - // Standard item usage - embed.setTitle(item ? `โœ… Used ${item.name}` : "โœ… Item Used!"); - embed.setDescription(otherMessages.join("\n") || "Effect applied."); + rarityKey = "NOTHING"; + } - if (isLootbox && item && item.iconUrl) { - if (isLocalAssetUrl(item.iconUrl)) { - const iconPath = join(process.cwd(), "bot/assets/graphics", item.iconUrl.replace(/^\/?assets\//, "")); - if (existsSync(iconPath)) { - const iconName = defaultName(item.iconUrl); - if (!files.find(f => f.name === iconName)) { - files.push(new AttachmentBuilder(iconPath, { name: iconName })); + 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 || ""; + if (description) description += "\n"; + description += `\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 })); } - embed.setThumbnail(`attachment://${iconName}`); + displayImageUrl = `attachment://${imageName}`; } } else { - const resolvedIconUrl = resolveAssetUrl(item.iconUrl); - if (resolvedIconUrl) embed.setThumbnail(resolvedIconUrl); + displayImageUrl = resolveAssetUrl(imgSource); + } + if (displayImageUrl) { + container.addMediaGalleryComponents( + new MediaGalleryBuilder().addItems( + new MediaGalleryItemBuilder().setURL(displayImageUrl) + ) + ); } } } - if (otherMessages.length > 0 && lootResult) { - embed.addFields({ name: "Other Effects", value: otherMessages.join("\n") }); + // 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 { embed, files }; -} - -function defaultName(path: string): string { - return path.split("/").pop() || "image.png"; + return { + components: [container] as any, + files, + flags: MessageFlags.IsComponentsV2, + embeds: undefined, + }; } diff --git a/shared/modules/inventory/effect.handlers.ts b/shared/modules/inventory/effect.handlers.ts index 0c5e66f..8fecc84 100644 --- a/shared/modules/inventory/effect.handlers.ts +++ b/shared/modules/inventory/effect.handlers.ts @@ -146,7 +146,8 @@ export const handleLootbox: EffectHandler = async (userId, effect: Extract 1 ? quantity + 'x ' : ''}**${item.name}**!` };