209 lines
7.6 KiB
TypeScript
209 lines
7.6 KiB
TypeScript
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<string, number> = {
|
|
"C": Colors.LightGrey,
|
|
"R": Colors.Blue,
|
|
"SR": Colors.Purple,
|
|
"SSR": Colors.Gold,
|
|
"CURRENCY": Colors.Green,
|
|
"XP": Colors.Aqua,
|
|
"NOTHING": Colors.DarkButNotBlack
|
|
};
|
|
|
|
const TitleMap: Record<string, string> = {
|
|
"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<number, { name: string; rarity: string }> }
|
|
) {
|
|
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<string, string[]> = {};
|
|
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<ButtonBuilder>().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";
|
|
}
|