import { createCommand } from "@shared/lib/utils"; 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, 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") .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) { await interaction.editReply({ embeds: [createWarningEmbed("Bots do not have inventories.", "Inventory Check")] }); return; } const user = await userService.getOrCreateUser(targetUser.id, targetUser.username); if (!user) { await interaction.editReply({ embeds: [createWarningEmbed("Failed to load user data.", "Error")] }); return; } const ownerId = user.id.toString(); const entries = await inventoryService.getInventory(ownerId); if (!entries || entries.length === 0) { await interaction.editReply(getEmptyInventoryMessage(user.username) as any); return; } let currentPage = 0; let selectedItemId: number | null = null; 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(() => {}); }); }