diff --git a/bot/commands/inventory/inventory.ts b/bot/commands/inventory/inventory.ts index cc9937b..0d80043 100644 --- a/bot/commands/inventory/inventory.ts +++ b/bot/commands/inventory/inventory.ts @@ -1,22 +1,83 @@ import { createCommand } from "@shared/lib/utils"; -import { SlashCommandBuilder } from "discord.js"; +import { SlashCommandBuilder, MessageFlags, ComponentType } from "discord.js"; import { inventoryService } from "@shared/modules/inventory/inventory.service"; import { userService } from "@shared/modules/user/user.service"; -import { createWarningEmbed } from "@lib/embeds"; -import { getInventoryEmbed } from "@/modules/inventory/inventory.view"; +import { createWarningEmbed, createErrorEmbed } from "@lib/embeds"; +import { + getInventoryListMessage, + getEmptyInventoryMessage, + getItemDetailMessage, + getDiscardConfirmMessage, + sortInventoryItems, + ITEMS_PER_PAGE, + type InventoryEntry, +} from "@/modules/inventory/inventory.view"; +import { getLootboxResultMessage } from "@/modules/inventory/inventory.view"; +import { + parseInventoryCustomId, + isInventoryInteraction, + executeItemUse, +} from "@/modules/inventory/inventory.interaction"; +import { UserError } from "@shared/lib/errors"; export const inventory = createCommand({ data: new SlashCommandBuilder() .setName("inventory") .setDescription("View your or another user's inventory") - .addUserOption(option => - option.setName("user") - .setDescription("User to view") - .setRequired(false) + .addSubcommand(sub => + sub.setName("list") + .setDescription("View your or another user's inventory") + .addUserOption(option => + option.setName("user") + .setDescription("User to view") + .setRequired(false) + ) + ) + .addSubcommand(sub => + sub.setName("view") + .setDescription("View details of a specific item") + .addNumberOption(option => + option.setName("item") + .setDescription("The item to view") + .setRequired(true) + .setAutocomplete(true) + ) ), execute: async (interaction) => { await interaction.deferReply(); + const viewerId = interaction.user.id; + const subcommand = interaction.options.getSubcommand(); + + if (subcommand === "view") { + // Direct item detail view + const itemId = interaction.options.getNumber("item", true); + const user = await userService.getOrCreateUser(viewerId, interaction.user.username); + if (!user) { + await interaction.editReply({ embeds: [createWarningEmbed("Failed to load user data.", "Error")] }); + return; + } + + const entries = await inventoryService.getInventory(user.id.toString()); + const entry = entries.find((e: any) => e.item.id === itemId); + if (!entry) { + await interaction.editReply({ embeds: [createWarningEmbed("Item not found in your inventory.", "Not Found")] }); + return; + } + + const ownerId = user.id.toString(); + let currentPage = 0; + let selectedItemId: number | null = itemId; + + const response = await interaction.editReply( + getItemDetailMessage(entry as InventoryEntry, viewerId, ownerId) as any + ); + + await setupCollector(interaction, response, viewerId, ownerId, user.username, currentPage, selectedItemId); + return; + } + + // "list" subcommand const targetUser = interaction.options.getUser("user") || interaction.user; if (targetUser.bot) { @@ -30,15 +91,209 @@ export const inventory = createCommand({ return; } - const items = await inventoryService.getInventory(user.id.toString()); + const ownerId = user.id.toString(); + const entries = await inventoryService.getInventory(ownerId); - if (!items || items.length === 0) { - await interaction.editReply({ embeds: [createWarningEmbed("Inventory is empty.", `${user.username}'s Inventory`)] }); + if (!entries || entries.length === 0) { + await interaction.editReply(getEmptyInventoryMessage(user.username) as any); return; } - const embed = getInventoryEmbed(items, user.username); + let currentPage = 0; + let selectedItemId: number | null = null; - await interaction.editReply({ embeds: [embed] }); - } + const response = await interaction.editReply( + getInventoryListMessage(entries as InventoryEntry[], user.username, currentPage, viewerId, ownerId) as any + ); + + await setupCollector(interaction, response, viewerId, ownerId, user.username, currentPage, selectedItemId); + }, + autocomplete: async (interaction) => { + const focusedValue = interaction.options.getFocused(); + const userId = interaction.user.id; + const results = await inventoryService.getAutocompleteItems(userId, focusedValue); + await interaction.respond(results); + }, }); + +async function setupCollector( + interaction: any, + response: any, + viewerId: string, + ownerId: string, + username: string, + initialPage: number, + initialItemId: number | null, +) { + let currentPage = initialPage; + let selectedItemId = initialItemId; + + const collector = response.createMessageComponentCollector({ + time: 120_000, + }); + + collector.on("collect", async (i: any) => { + if (i.user.id !== viewerId) return; + + const parsed = parseInventoryCustomId(i.customId); + if (!parsed) return; + + try { + await i.deferUpdate(); + + // Re-fetch inventory for fresh data + const entries = await inventoryService.getInventory(ownerId); + const sorted = sortInventoryItems(entries as InventoryEntry[]); + + switch (parsed.action) { + case "select": { + const itemId = parseInt(i.values[0]); + const entry = sorted.find(e => e.item.id === itemId); + if (!entry) break; + selectedItemId = itemId; + await interaction.editReply( + getItemDetailMessage(entry, viewerId, ownerId) + ); + break; + } + + case "prev": { + currentPage = Math.max(0, currentPage - 1); + selectedItemId = null; + await interaction.editReply( + getInventoryListMessage(sorted, username, currentPage, viewerId, ownerId) + ); + break; + } + + case "next": { + currentPage = currentPage + 1; + selectedItemId = null; + await interaction.editReply( + getInventoryListMessage(sorted, username, currentPage, viewerId, ownerId) + ); + break; + } + + case "back": { + selectedItemId = null; + if (sorted.length === 0) { + await interaction.editReply(getEmptyInventoryMessage(username)); + } else { + await interaction.editReply( + getInventoryListMessage(sorted, username, currentPage, viewerId, ownerId) + ); + } + break; + } + + case "use": { + if (viewerId !== ownerId || !selectedItemId) break; + try { + const result = await executeItemUse(i, viewerId, selectedItemId); + const message = getLootboxResultMessage(result.results, result.item); + await interaction.editReply(message as any); + + // After showing result, wait briefly then return to detail or list + setTimeout(async () => { + try { + const freshEntries = await inventoryService.getInventory(ownerId); + const freshSorted = sortInventoryItems(freshEntries as InventoryEntry[]); + const freshEntry = freshSorted.find(e => e.item.id === selectedItemId); + + if (freshEntry) { + await interaction.editReply( + getItemDetailMessage(freshEntry, viewerId, ownerId) + ); + } else { + selectedItemId = null; + if (freshSorted.length === 0) { + await interaction.editReply(getEmptyInventoryMessage(username)); + } else { + await interaction.editReply( + getInventoryListMessage(freshSorted, username, currentPage, viewerId, ownerId) + ); + } + } + } catch {} + }, 3000); + } catch (error) { + if (error instanceof UserError) { + await interaction.editReply({ + embeds: [createErrorEmbed(error.message)], + components: [], + flags: undefined, + }); + } else { + throw error; + } + } + break; + } + + case "discard": { + if (viewerId !== ownerId || !selectedItemId) break; + const entry = sorted.find(e => e.item.id === selectedItemId); + if (!entry) break; + await interaction.editReply( + getDiscardConfirmMessage(entry, viewerId) + ); + break; + } + + case "discard_confirm": { + if (viewerId !== ownerId || !selectedItemId) break; + try { + await inventoryService.removeItem(ownerId, selectedItemId, 1n); + + const freshEntries = await inventoryService.getInventory(ownerId); + const freshSorted = sortInventoryItems(freshEntries as InventoryEntry[]); + const freshEntry = freshSorted.find(e => e.item.id === selectedItemId); + + if (freshEntry) { + await interaction.editReply( + getItemDetailMessage(freshEntry, viewerId, ownerId) + ); + } else { + selectedItemId = null; + if (freshSorted.length === 0) { + await interaction.editReply(getEmptyInventoryMessage(username)); + } else { + await interaction.editReply( + getInventoryListMessage(freshSorted, username, currentPage, viewerId, ownerId) + ); + } + } + } catch (error) { + if (error instanceof UserError) { + await interaction.editReply({ + embeds: [createErrorEmbed(error.message)], + components: [], + flags: undefined, + }); + } else { + throw error; + } + } + break; + } + + case "discard_cancel": { + if (!selectedItemId) break; + const entry = sorted.find(e => e.item.id === selectedItemId); + if (!entry) break; + await interaction.editReply( + getItemDetailMessage(entry, viewerId, ownerId) + ); + break; + } + } + } catch (error) { + console.error("Inventory interaction error:", error); + } + }); + + collector.on("end", () => { + interaction.editReply({ components: [] }).catch(() => {}); + }); +}