Files
aurorabot/bot/modules/economy/shop.view.ts
syntaxbullet 25a0bd3431
Some checks failed
Deploy to Production / test (push) Failing after 29s
Sign panel sessions and isolate test runs
- Replace in-memory auth sessions with signed cookies and signed OAuth state
- Add auth route coverage and update panel/web server wiring
- Switch test script to per-file Bun processes and clean up type checks
2026-04-09 21:44:05 +02:00

217 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
AttachmentBuilder,
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";
import { getRarityConfig, defaultName, stripQuery } from "@shared/lib/rarity";
import { SHOP_CUSTOM_IDS } from "./economy.types";
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", stripQuery(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", stripQuery(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(getRarityConfig(item.rarity || "C").color);
// 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))
)
);
}
// Create buy button (used in either main or loot container)
const buyButton = new ButtonBuilder()
.setCustomId(SHOP_CUSTOM_IDS.BUY(item.id))
.setLabel(`Purchase for ${item.price} 🪙`)
.setStyle(ButtonStyle.Success)
.setEmoji("🛒");
// 2. Loot Table (if applicable) — separate Container with blurple accent
const lootboxEffect = item.usageData?.effects?.find((e: any) => e.type === EffectType.LOOTBOX);
if (lootboxEffect) {
const pool = lootboxEffect.pool as LootTableItem[];
const totalWeight = pool.reduce((sum: number, i: LootTableItem) => sum + i.weight, 0);
const lootContainer = new ContainerBuilder().setAccentColor(0x5865F2);
lootContainer.addTextDisplayComponents(
new TextDisplayBuilder().setContent("## 🎁 Loot Table")
);
// Group drops by rarity tier with aggregated percentages
const tiers: Record<string, { items: string[]; totalChance: number }> = {};
for (const drop of pool) {
const chance = (drop.weight / totalWeight) * 100;
let line = "";
let rarity = "C";
switch (drop.type as any) {
case LootType.CURRENCY: {
const amt = (drop.minAmount != null && drop.maxAmount != null)
? `${drop.minAmount} ${drop.maxAmount}`
: (Array.isArray(drop.amount) ? `${drop.amount[0]} ${drop.amount[1]}` : `${drop.amount || 0}`);
line = `${amt} 🪙`;
rarity = "CURRENCY";
break;
}
case LootType.XP: {
const amt = (drop.minAmount != null && drop.maxAmount != null)
? `${drop.minAmount} ${drop.maxAmount}`
: (Array.isArray(drop.amount) ? `${drop.amount[0]} ${drop.amount[1]}` : `${drop.amount || 0}`);
line = `${amt} XP`;
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} ×${drop.amount || 1}`;
rarity = i.rarity;
} else {
line = `Unknown Item`;
rarity = "C";
}
break;
}
case LootType.NOTHING: {
line = "Nothing";
rarity = "NOTHING";
break;
}
}
if (line) {
if (!tiers[rarity]) tiers[rarity] = { items: [], totalChance: 0 };
const tier = tiers[rarity]!;
tier.items.push(line);
tier.totalChance += chance;
}
}
const order = ["SSR", "SR", "R", "C", "CURRENCY", "XP", "NOTHING"];
let isFirst = true;
for (const rarity of order) {
const tier = tiers[rarity];
if (!tier || tier.items.length === 0) continue;
if (!isFirst) {
lootContainer.addSeparatorComponents(
new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small)
);
}
isFirst = false;
const config = getRarityConfig(rarity);
const chanceStr = tier.totalChance < 0.1 ? "<0.1" : tier.totalChance.toFixed(1);
lootContainer.addTextDisplayComponents(
new TextDisplayBuilder().setContent(
`${config.emoji} **${config.label}** — ${chanceStr}%`
),
new TextDisplayBuilder().setContent(tier.items.join(", "))
);
}
// Purchase button inside loot table container
lootContainer.addActionRowComponents(
new ActionRowBuilder<ButtonBuilder>().addComponents(buyButton)
);
containers.push(mainContainer);
containers.push(lootContainer);
} else {
// Non-lootbox items: purchase button stays in main container
mainContainer.addActionRowComponents(
new ActionRowBuilder<ButtonBuilder>().addComponents(buyButton)
);
containers.push(mainContainer);
}
return {
components: containers as any,
files,
flags: MessageFlags.IsComponentsV2
};
}