feat: rewrite lootbox pull results with Components V2 and separate icon/image URLs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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<string, number> = {
|
||||
'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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -146,7 +146,8 @@ export const handleLootbox: EffectHandler = async (userId, effect: Extract<Valid
|
||||
name: item.name,
|
||||
rarity: item.rarity,
|
||||
description: item.description,
|
||||
image: item.imageUrl || item.iconUrl
|
||||
iconUrl: item.iconUrl,
|
||||
imageUrl: item.imageUrl,
|
||||
},
|
||||
message: winner.message || `You found ${quantity > 1 ? quantity + 'x ' : ''}**${item.name}**!`
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user