Update shop command part1

This commit is contained in:
2025-10-08 21:53:46 -05:00
parent 67b187c3c0
commit 063b61bca2

View File

@@ -1,5 +1,5 @@
import type { CommandMessage } from '../../../core/types/commands'; import type { CommandMessage } from "../../../core/types/commands";
import type Amayo from '../../../core/client'; import type Amayo from "../../../core/client";
import { import {
Message, Message,
ButtonInteraction, ButtonInteraction,
@@ -7,18 +7,18 @@ import {
ComponentType, ComponentType,
ButtonStyle, ButtonStyle,
MessageFlags, MessageFlags,
StringSelectMenuInteraction StringSelectMenuInteraction,
} from 'discord.js'; } from "discord.js";
import { prisma } from '../../../core/database/prisma'; import { prisma } from "../../../core/database/prisma";
import { getOrCreateWallet, buyFromOffer } from '../../../game/economy/service'; import { getOrCreateWallet, buyFromOffer } from "../../../game/economy/service";
import type { DisplayComponentContainer } from '../../../core/types/displayComponents'; import type { DisplayComponentContainer } from "../../../core/types/displayComponents";
import type { ItemProps } from '../../../game/economy/types'; import type { ItemProps } from "../../../game/economy/types";
import { formatItemLabel, resolveItemIcon } from './_helpers'; import { formatItemLabel, resolveItemIcon } from "./_helpers";
const ITEMS_PER_PAGE = 5; const ITEMS_PER_PAGE = 5;
function parseItemProps(json: unknown): ItemProps { function parseItemProps(json: unknown): ItemProps {
if (!json || typeof json !== 'object') return {}; if (!json || typeof json !== "object") return {};
return json as ItemProps; return json as ItemProps;
} }
@@ -30,36 +30,37 @@ function formatPrice(price: any): string {
parts.push(`📦 ${item.itemKey} x${item.qty}`); parts.push(`📦 ${item.itemKey} x${item.qty}`);
} }
} }
return parts.join(' + ') || '¿Gratis?'; return parts.join(" + ") || "¿Gratis?";
} }
function getItemIcon(props: ItemProps, category?: string): string { function getItemIcon(props: ItemProps, category?: string): string {
if (props.tool) { if (props.tool) {
const t = props.tool.type; const t = props.tool.type;
if (t === 'pickaxe') return '⛏️'; if (t === "pickaxe") return "⛏️";
if (t === 'rod') return '🎣'; if (t === "rod") return "🎣";
if (t === 'sword') return '🗡️'; if (t === "sword") return "🗡️";
if (t === 'bow') return '🏹'; if (t === "bow") return "🏹";
if (t === 'halberd') return '⚔️'; if (t === "halberd") return "⚔️";
if (t === 'net') return '🕸️'; if (t === "net") return "🕸️";
return '🔧'; return "🔧";
} }
if (props.damage && props.damage > 0) return '⚔️'; if (props.damage && props.damage > 0) return "⚔️";
if (props.defense && props.defense > 0) return '🛡️'; if (props.defense && props.defense > 0) return "🛡️";
if (props.food) return '🍖'; if (props.food) return "🍖";
if (props.chest) return '📦'; if (props.chest) return "📦";
if (category === 'consumables') return '🧪'; if (category === "consumables") return "🧪";
if (category === 'materials') return '🔨'; if (category === "materials") return "🔨";
return '📦'; return "📦";
} }
export const command: CommandMessage = { export const command: CommandMessage = {
name: 'tienda', name: "tienda",
type: 'message', type: "message",
aliases: ['shop', 'store'], aliases: ["shop", "store"],
cooldown: 5, cooldown: 5,
description: 'Abre la tienda y navega por las ofertas disponibles con un panel interactivo.', description:
usage: 'tienda [categoria]', "Abre la tienda y navega por las ofertas disponibles con un panel interactivo.",
usage: "tienda [categoria]",
run: async (message, args, _client: Amayo) => { run: async (message, args, _client: Amayo) => {
const userId = message.author.id; const userId = message.author.id;
const guildId = message.guild!.id; const guildId = message.guild!.id;
@@ -77,89 +78,104 @@ export const command: CommandMessage = {
{ startAt: null, endAt: null }, { startAt: null, endAt: null },
{ startAt: { lte: now }, endAt: { gte: now } }, { startAt: { lte: now }, endAt: { gte: now } },
{ startAt: { lte: now }, endAt: null }, { startAt: { lte: now }, endAt: null },
{ startAt: null, endAt: { gte: now } } { startAt: null, endAt: { gte: now } },
] ],
}, },
include: { item: true }, include: { item: true },
orderBy: { createdAt: 'desc' } orderBy: { createdAt: "desc" },
}); });
if (offers.length === 0) { if (offers.length === 0) {
await message.reply('🏪 La tienda está vacía. ¡Vuelve más tarde!'); await message.reply(
"<a:seven:1425666197466255481> La tienda está vacía. ¡Vuelve más tarde!"
);
return; return;
} }
// Filtrar por categoría si se proporciona // Filtrar por categoría si se proporciona
const categoryFilter = args[0]?.trim().toLowerCase(); const categoryFilter = args[0]?.trim().toLowerCase();
const filteredOffers = categoryFilter const filteredOffers = categoryFilter
? offers.filter(o => o.item.category?.toLowerCase().includes(categoryFilter)) ? offers.filter((o) =>
o.item.category?.toLowerCase().includes(categoryFilter)
)
: offers; : offers;
if (filteredOffers.length === 0) { if (filteredOffers.length === 0) {
await message.reply(`🏪 No hay ofertas en la categoría "${categoryFilter}".`); await message.reply(
`<a:seven:1425666197466255481> No hay ofertas en la categoría "${categoryFilter}".`
);
return; return;
} }
// Estado inicial // Estado inicial
const sessionState = { const sessionState = {
currentPage: 1, currentPage: 1,
selectedOfferId: null as string | null selectedOfferId: null as string | null,
}; };
const shopMessage = await message.reply({ const shopMessage = await message.reply({
flags: MessageFlags.SuppressEmbeds | 32768, flags: MessageFlags.SuppressEmbeds | 32768,
components: await buildShopPanel(filteredOffers, sessionState.currentPage, wallet.coins, sessionState.selectedOfferId) components: await buildShopPanel(
filteredOffers,
sessionState.currentPage,
wallet.coins,
sessionState.selectedOfferId
),
}); });
// Collector para interacciones // Collector para interacciones
const collector = shopMessage.createMessageComponentCollector({ const collector = shopMessage.createMessageComponentCollector({
time: 300000, // 5 minutos time: 300000, // 5 minutos
filter: (i: MessageComponentInteraction) => i.user.id === message.author.id filter: (i: MessageComponentInteraction) =>
i.user.id === message.author.id,
}); });
collector.on('collect', async (interaction: MessageComponentInteraction) => { collector.on(
try { "collect",
if (interaction.isButton()) { async (interaction: MessageComponentInteraction) => {
await handleButtonInteraction( try {
interaction as ButtonInteraction, if (interaction.isButton()) {
filteredOffers, await handleButtonInteraction(
sessionState, interaction as ButtonInteraction,
userId, filteredOffers,
guildId, sessionState,
shopMessage, userId,
collector guildId,
); shopMessage,
} else if (interaction.isStringSelectMenu()) { collector
await handleSelectInteraction( );
interaction as StringSelectMenuInteraction, } else if (interaction.isStringSelectMenu()) {
filteredOffers, await handleSelectInteraction(
sessionState.currentPage, interaction as StringSelectMenuInteraction,
userId, filteredOffers,
guildId, sessionState.currentPage,
shopMessage userId,
); guildId,
} shopMessage
} catch (error: any) { );
console.error('Error handling shop interaction:', error); }
if (!interaction.replied && !interaction.deferred) { } catch (error: any) {
await interaction.reply({ console.error("Error handling shop interaction:", error);
content: `❌ Error: ${error?.message ?? error}`, if (!interaction.replied && !interaction.deferred) {
flags: MessageFlags.Ephemeral await interaction.reply({
}); content: `❌ Error: ${error?.message ?? error}`,
flags: MessageFlags.Ephemeral,
});
}
} }
} }
}); );
collector.on('end', async (_, reason) => { collector.on("end", async (_, reason) => {
if (reason === 'time') { if (reason === "time") {
try { try {
await shopMessage.edit({ await shopMessage.edit({
components: await buildExpiredPanel() components: await buildExpiredPanel(),
}); });
} catch {} } catch {}
} }
}); });
} },
}; };
async function buildShopPanel( async function buildShopPanel(
@@ -175,7 +191,7 @@ async function buildShopPanel(
// Encontrar la oferta seleccionada // Encontrar la oferta seleccionada
const selectedOffer = selectedOfferId const selectedOffer = selectedOfferId
? offers.find(o => o.id === selectedOfferId) ? offers.find((o) => o.id === selectedOfferId)
: null; : null;
// Container principal // Container principal
@@ -185,85 +201,109 @@ async function buildShopPanel(
components: [ components: [
{ {
type: 10, type: 10,
content: `🏪 **TIENDA** | Página ${safePage}/${totalPages}\n💰 Tus Monedas: **${userCoins}**` content: `# <a:seven:1425666197466255481> Tienda - Ofertas Disponibles`,
},
{
type: 10,
content: `-# <:coin:1425667511013081169> Monedas: **${userCoins}**`,
}, },
{ {
type: 14, type: 14,
divider: true, divider: true,
spacing: 2 spacing: 2,
} },
] ],
}; };
// Si hay una oferta seleccionada, mostrar detalles // Si hay una oferta seleccionada, mostrar detalles
if (selectedOffer) { if (selectedOffer) {
const item = selectedOffer.item; const item = selectedOffer.item;
const props = parseItemProps(item.props); const props = parseItemProps(item.props);
const label = formatItemLabel(item, { fallbackIcon: getItemIcon(props, item.category), bold: true }); const label = formatItemLabel(item, {
fallbackIcon: getItemIcon(props, item.category),
bold: true,
});
const price = formatPrice(selectedOffer.price); const price = formatPrice(selectedOffer.price);
// Stock info // Stock info
let stockInfo = ''; let stockInfo = "";
if (selectedOffer.stock != null) { if (selectedOffer.stock != null) {
stockInfo = `\n📊 Stock: ${selectedOffer.stock}`; stockInfo = `\n<:clipboard:1425669350316048435> Stock: ${selectedOffer.stock}`;
} }
if (selectedOffer.perUserLimit != null) { if (selectedOffer.perUserLimit != null) {
const purchased = await prisma.shopPurchase.aggregate({ const purchased = await prisma.shopPurchase.aggregate({
where: { offerId: selectedOffer.id }, where: { offerId: selectedOffer.id },
_sum: { qty: true } _sum: { qty: true },
}); });
const userPurchased = purchased._sum.qty ?? 0; const userPurchased = purchased._sum.qty ?? 0;
stockInfo += `\n👤 Límite por usuario: ${userPurchased}/${selectedOffer.perUserLimit}`; stockInfo += `\n<:Sup_urg:1420535068056748042> Límite por usuario: ${userPurchased}/${selectedOffer.perUserLimit}`;
} }
// Stats del item // Stats del item
let statsInfo = ''; let statsInfo = "";
if (props.damage) statsInfo += `\n⚔ Daño: +${props.damage}`; if (props.damage)
if (props.defense) statsInfo += `\n🛡 Defensa: +${props.defense}`; statsInfo += `\n<:damage:1425670476449189998> Daño: +${props.damage}`;
if (props.maxHpBonus) statsInfo += `\n❤ HP Bonus: +${props.maxHpBonus}`; if (props.defense)
if (props.tool) statsInfo += `\n🔧 Herramienta: ${props.tool.type} T${props.tool.tier ?? 1}`; statsInfo += `\n<:defens:1425670433910427862> Defensa: +${props.defense}`;
if (props.food && props.food.healHp) statsInfo += `\n🍖 Cura: ${props.food.healHp} HP`; if (props.maxHpBonus)
statsInfo += `\n<:healbonus:1425671499792121877> HP Bonus: +${props.maxHpBonus}`;
if (props.tool)
statsInfo += `\n<:table:1425673712312782879> Herramienta: ${
props.tool.type
} T${props.tool.tier ?? 1}`;
if (props.food && props.food.healHp)
statsInfo += `\n<:cure:1425671519639572510> Cura: ${props.food.healHp} HP`;
container.components.push({ container.components.push({
type: 10, type: 10,
content: `${label}\n\n${item.description || 'Sin descripción'}${statsInfo}\n\n💰 Precio: ${price}${stockInfo}` content: `${label}\n\n${
item.description || "Sin descripción"
}${statsInfo}\n\n<:price:1425673879094820906> Precio: ${price}${stockInfo}`,
}); });
container.components.push({ container.components.push({
type: 14, type: 14,
divider: true, divider: true,
spacing: 1 spacing: 1,
}); });
} }
// Lista de ofertas en la página // Lista de ofertas en la página
container.components.push({ container.components.push({
type: 10, type: 10,
content: selectedOffer ? '📋 **Otras Ofertas:**' : '📋 **Ofertas Disponibles:**' content: selectedOffer
? "<:clipboard:1425669350316048435> **Otras Ofertas:**"
: "<:clipboard:1425669350316048435> **Ofertas Disponibles:**",
}); });
for (const offer of pageOffers) { for (const offer of pageOffers) {
const item = offer.item; const item = offer.item;
const props = parseItemProps(item.props); const props = parseItemProps(item.props);
const label = formatItemLabel(item, { fallbackIcon: getItemIcon(props, item.category), bold: true }); const label = formatItemLabel(item, {
fallbackIcon: getItemIcon(props, item.category),
bold: true,
});
const price = formatPrice(offer.price); const price = formatPrice(offer.price);
const isSelected = selectedOfferId === offer.id; const isSelected = selectedOfferId === offer.id;
const stockText = offer.stock != null ? ` (${offer.stock} disponibles)` : ''; const stockText =
const selectedMark = isSelected ? ' ✓' : ''; offer.stock != null ? ` (${offer.stock} disponibles)` : "";
const selectedMark = isSelected ? " ✓" : "";
container.components.push({ container.components.push({
type: 9, type: 9,
components: [{ components: [
type: 10, {
content: `${label}${selectedMark}\n💰 ${price}${stockText}` type: 10,
}], content: `${label}${selectedMark}\n<:coin:1425667511013081169> ${price}${stockText}`,
},
],
accessory: { accessory: {
type: 2, type: 2,
style: isSelected ? ButtonStyle.Success : ButtonStyle.Primary, style: isSelected ? ButtonStyle.Success : ButtonStyle.Primary,
label: isSelected ? 'Seleccionado' : 'Ver', label: isSelected ? "Seleccionado" : "Ver",
custom_id: `shop_view_${offer.id}` custom_id: `shop_view_${offer.id}`,
} },
}); });
} }
@@ -274,25 +314,25 @@ async function buildShopPanel(
{ {
type: ComponentType.Button, type: ComponentType.Button,
style: ButtonStyle.Secondary, style: ButtonStyle.Secondary,
label: '◀️ Anterior', label: "◀️ Anterior",
custom_id: 'shop_prev_page', custom_id: "shop_prev_page",
disabled: safePage <= 1 disabled: safePage <= 1,
}, },
{ {
type: ComponentType.Button, type: ComponentType.Button,
style: ButtonStyle.Secondary, style: ButtonStyle.Secondary,
label: `Página ${safePage}/${totalPages}`, label: `Página ${safePage}/${totalPages}`,
custom_id: 'shop_current_page', custom_id: "shop_current_page",
disabled: true disabled: true,
}, },
{ {
type: ComponentType.Button, type: ComponentType.Button,
style: ButtonStyle.Secondary, style: ButtonStyle.Secondary,
label: 'Siguiente ▶️', label: "Siguiente ▶️",
custom_id: 'shop_next_page', custom_id: "shop_next_page",
disabled: safePage >= totalPages disabled: safePage >= totalPages,
} },
] ],
}; };
const actionRow2 = { const actionRow2 = {
@@ -301,30 +341,30 @@ async function buildShopPanel(
{ {
type: ComponentType.Button, type: ComponentType.Button,
style: ButtonStyle.Success, style: ButtonStyle.Success,
label: '🛒 Comprar (x1)', label: "🛒 Comprar (x1)",
custom_id: 'shop_buy_1', custom_id: "shop_buy_1",
disabled: !selectedOfferId disabled: !selectedOfferId,
}, },
{ {
type: ComponentType.Button, type: ComponentType.Button,
style: ButtonStyle.Success, style: ButtonStyle.Success,
label: '🛒 Comprar (x5)', label: "🛒 Comprar (x5)",
custom_id: 'shop_buy_5', custom_id: "shop_buy_5",
disabled: !selectedOfferId disabled: !selectedOfferId,
}, },
{ {
type: ComponentType.Button, type: ComponentType.Button,
style: ButtonStyle.Primary, style: ButtonStyle.Primary,
label: '🔄 Actualizar', label: "🔄 Actualizar",
custom_id: 'shop_refresh' custom_id: "shop_refresh",
}, },
{ {
type: ComponentType.Button, type: ComponentType.Button,
style: ButtonStyle.Danger, style: ButtonStyle.Danger,
label: '❌ Cerrar', label: "❌ Cerrar",
custom_id: 'shop_close' custom_id: "shop_close",
} },
] ],
}; };
return [container, actionRow1, actionRow2]; return [container, actionRow1, actionRow2];
@@ -342,78 +382,95 @@ async function handleButtonInteraction(
const customId = interaction.customId; const customId = interaction.customId;
// Ver detalles de un item // Ver detalles de un item
if (customId.startsWith('shop_view_')) { if (customId.startsWith("shop_view_")) {
const offerId = customId.replace('shop_view_', ''); const offerId = customId.replace("shop_view_", "");
const wallet = await getOrCreateWallet(userId, guildId); const wallet = await getOrCreateWallet(userId, guildId);
sessionState.selectedOfferId = offerId; sessionState.selectedOfferId = offerId;
await interaction.update({ await interaction.update({
components: await buildShopPanel(offers, sessionState.currentPage, wallet.coins, sessionState.selectedOfferId) components: await buildShopPanel(
offers,
sessionState.currentPage,
wallet.coins,
sessionState.selectedOfferId
),
}); });
return; return;
} }
// Comprar // Comprar
if (customId === 'shop_buy_1' || customId === 'shop_buy_5') { if (customId === "shop_buy_1" || customId === "shop_buy_5") {
const selectedOfferId = sessionState.selectedOfferId; const selectedOfferId = sessionState.selectedOfferId;
if (!selectedOfferId) { if (!selectedOfferId) {
await interaction.reply({ await interaction.reply({
content: '❌ Primero selecciona un item.', content: "❌ Primero selecciona un item.",
flags: MessageFlags.Ephemeral flags: MessageFlags.Ephemeral,
}); });
return; return;
} }
const qty = customId === 'shop_buy_1' ? 1 : 5; const qty = customId === "shop_buy_1" ? 1 : 5;
try { try {
await interaction.deferUpdate(); await interaction.deferUpdate();
const result = await buyFromOffer(userId, guildId, selectedOfferId, qty); const result = await buyFromOffer(userId, guildId, selectedOfferId, qty);
const wallet = await getOrCreateWallet(userId, guildId); const wallet = await getOrCreateWallet(userId, guildId);
const purchaseLabel = formatItemLabel(result.item, { fallbackIcon: resolveItemIcon(result.item.icon) }); const purchaseLabel = formatItemLabel(result.item, {
fallbackIcon: resolveItemIcon(result.item.icon),
});
await interaction.followUp({ await interaction.followUp({
content: `✅ **Compra exitosa!**\n🛒 ${purchaseLabel} x${result.qty}\n💰 Te quedan: ${wallet.coins} monedas`, content: `✅ **Compra exitosa!**\n🛒 ${purchaseLabel} x${result.qty}\n<:coin:1425667511013081169> Te quedan: ${wallet.coins} monedas`,
flags: MessageFlags.Ephemeral flags: MessageFlags.Ephemeral,
}); });
// Actualizar tienda // Actualizar tienda
await shopMessage.edit({ await shopMessage.edit({
components: await buildShopPanel(offers, sessionState.currentPage, wallet.coins, sessionState.selectedOfferId) components: await buildShopPanel(
offers,
sessionState.currentPage,
wallet.coins,
sessionState.selectedOfferId
),
}); });
} catch (error: any) { } catch (error: any) {
await interaction.followUp({ await interaction.followUp({
content: `❌ No se pudo comprar: ${error?.message ?? error}`, content: `❌ No se pudo comprar: ${error?.message ?? error}`,
flags: MessageFlags.Ephemeral flags: MessageFlags.Ephemeral,
}); });
} }
return; return;
} }
// Actualizar // Actualizar
if (customId === 'shop_refresh') { if (customId === "shop_refresh") {
const wallet = await getOrCreateWallet(userId, guildId); const wallet = await getOrCreateWallet(userId, guildId);
await interaction.update({ await interaction.update({
components: await buildShopPanel(offers, sessionState.currentPage, wallet.coins, sessionState.selectedOfferId) components: await buildShopPanel(
offers,
sessionState.currentPage,
wallet.coins,
sessionState.selectedOfferId
),
}); });
return; return;
} }
// Cerrar // Cerrar
if (customId === 'shop_close') { if (customId === "shop_close") {
await interaction.update({ await interaction.update({
components: await buildClosedPanel() components: await buildClosedPanel(),
}); });
collector.stop(); collector.stop();
return; return;
} }
// Navegación de páginas (ya manejado en el collect) // Navegación de páginas (ya manejado en el collect)
if (customId === 'shop_prev_page' || customId === 'shop_next_page') { if (customId === "shop_prev_page" || customId === "shop_next_page") {
const wallet = await getOrCreateWallet(userId, guildId); const wallet = await getOrCreateWallet(userId, guildId);
let newPage = sessionState.currentPage; let newPage = sessionState.currentPage;
if (customId === 'shop_prev_page') { if (customId === "shop_prev_page") {
newPage = Math.max(1, sessionState.currentPage - 1); newPage = Math.max(1, sessionState.currentPage - 1);
} else { } else {
const totalPages = Math.ceil(offers.length / ITEMS_PER_PAGE); const totalPages = Math.ceil(offers.length / ITEMS_PER_PAGE);
@@ -423,7 +480,12 @@ async function handleButtonInteraction(
sessionState.currentPage = newPage; sessionState.currentPage = newPage;
await interaction.update({ await interaction.update({
components: await buildShopPanel(offers, sessionState.currentPage, wallet.coins, sessionState.selectedOfferId) components: await buildShopPanel(
offers,
sessionState.currentPage,
wallet.coins,
sessionState.selectedOfferId
),
}); });
return; return;
} }
@@ -439,8 +501,8 @@ async function handleSelectInteraction(
): Promise<void> { ): Promise<void> {
// Si implementas un select menu, manejar aquí // Si implementas un select menu, manejar aquí
await interaction.reply({ await interaction.reply({
content: 'Select menu no implementado aún', content: "Select menu no implementado aún",
flags: MessageFlags.Ephemeral flags: MessageFlags.Ephemeral,
}); });
} }
@@ -451,18 +513,19 @@ async function buildExpiredPanel(): Promise<any[]> {
components: [ components: [
{ {
type: 10, type: 10,
content: '⏰ **Tienda Expirada**' content: "⏰ **Tienda Expirada**",
}, },
{ {
type: 14, type: 14,
divider: true, divider: true,
spacing: 1 spacing: 1,
}, },
{ {
type: 10, type: 10,
content: 'La sesión de tienda ha expirado.\nUsa `!tienda` nuevamente para ver las ofertas.' content:
} "La sesión de tienda ha expirado.\nUsa `!tienda` nuevamente para ver las ofertas.",
] },
],
}; };
return [container]; return [container];
@@ -475,18 +538,18 @@ async function buildClosedPanel(): Promise<any[]> {
components: [ components: [
{ {
type: 10, type: 10,
content: '✅ **Tienda Cerrada**' content: "✅ **Tienda Cerrada**",
}, },
{ {
type: 14, type: 14,
divider: true, divider: true,
spacing: 1 spacing: 1,
}, },
{ {
type: 10, type: 10,
content: '¡Gracias por visitar la tienda!\nVuelve pronto. 🛒' content: "¡Gracias por visitar la tienda!\nVuelve pronto. 🛒",
} },
] ],
}; };
return [container]; return [container];