190 lines
6.9 KiB
TypeScript
190 lines
6.9 KiB
TypeScript
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,
|
||
};
|
||
}
|