feat: rework shop loot table into two-container Components V2 layout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,6 @@ import {
|
|||||||
ButtonBuilder,
|
ButtonBuilder,
|
||||||
ButtonStyle,
|
ButtonStyle,
|
||||||
AttachmentBuilder,
|
AttachmentBuilder,
|
||||||
Colors,
|
|
||||||
ContainerBuilder,
|
ContainerBuilder,
|
||||||
SectionBuilder,
|
SectionBuilder,
|
||||||
TextDisplayBuilder,
|
TextDisplayBuilder,
|
||||||
@@ -19,27 +18,7 @@ import { join } from "path";
|
|||||||
import { existsSync } from "fs";
|
import { existsSync } from "fs";
|
||||||
import { LootType, EffectType } from "@shared/lib/constants";
|
import { LootType, EffectType } from "@shared/lib/constants";
|
||||||
import type { LootTableItem } from "@shared/lib/types";
|
import type { LootTableItem } from "@shared/lib/types";
|
||||||
|
import { getRarityConfig, defaultName } from "@shared/lib/rarity";
|
||||||
// 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(
|
export function getShopListingMessage(
|
||||||
item: {
|
item: {
|
||||||
@@ -89,7 +68,7 @@ export function getShopListingMessage(
|
|||||||
|
|
||||||
// 1. Main Container
|
// 1. Main Container
|
||||||
const mainContainer = new ContainerBuilder()
|
const mainContainer = new ContainerBuilder()
|
||||||
.setAccentColor(RarityColors[item.rarity || "C"] || Colors.Green);
|
.setAccentColor(getRarityConfig(item.rarity || "C").color);
|
||||||
|
|
||||||
// Header Section
|
// Header Section
|
||||||
const infoSection = new SectionBuilder()
|
const infoSection = new SectionBuilder()
|
||||||
@@ -119,82 +98,113 @@ export function getShopListingMessage(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Loot Table (if applicable)
|
// Create buy button (used in either main or loot container)
|
||||||
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()
|
const buyButton = new ButtonBuilder()
|
||||||
.setCustomId(`shop_buy_${item.id}`)
|
.setCustomId(`shop_buy_${item.id}`)
|
||||||
.setLabel(`Purchase for ${item.price} 🪙`)
|
.setLabel(`Purchase for ${item.price} 🪙`)
|
||||||
.setStyle(ButtonStyle.Success)
|
.setStyle(ButtonStyle.Success)
|
||||||
.setEmoji("🛒");
|
.setEmoji("🛒");
|
||||||
|
|
||||||
|
// 2. Loot Table (if applicable) — separate Container with blurple accent
|
||||||
|
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: 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 };
|
||||||
|
tiers[rarity].items.push(line);
|
||||||
|
tiers[rarity].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.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(
|
mainContainer.addActionRowComponents(
|
||||||
new ActionRowBuilder<ButtonBuilder>().addComponents(buyButton)
|
new ActionRowBuilder<ButtonBuilder>().addComponents(buyButton)
|
||||||
);
|
);
|
||||||
|
|
||||||
containers.push(mainContainer);
|
containers.push(mainContainer);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
components: containers as any,
|
components: containers as any,
|
||||||
@@ -202,7 +212,3 @@ export function getShopListingMessage(
|
|||||||
flags: MessageFlags.IsComponentsV2
|
flags: MessageFlags.IsComponentsV2
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultName(path: string): string {
|
|
||||||
return path.split("/").pop() || "image.png";
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user