Some checks failed
Deploy to Production / test (push) Failing after 31s
- Replace setTimeout race in use-item flow with explicit Back button - Fix collector end handler to re-render current view instead of blanking - Add appendUseBackButton helper to attach navigation to use results - Remove unused isInventoryInteraction import - Fix rarity test type assertions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
323 lines
13 KiB
TypeScript
323 lines
13 KiB
TypeScript
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,
|
|
appendUseBackButton,
|
|
sortInventoryItems,
|
|
ITEMS_PER_PAGE,
|
|
type InventoryEntry,
|
|
} from "@/modules/inventory/inventory.view";
|
|
import { getLootboxResultMessage } from "@/modules/inventory/inventory.view";
|
|
import {
|
|
parseInventoryCustomId,
|
|
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(appendUseBackButton(message, viewerId) as any);
|
|
} catch (error) {
|
|
if (error instanceof UserError) {
|
|
await interaction.editReply({
|
|
embeds: [createErrorEmbed(error.message)],
|
|
components: [],
|
|
flags: undefined,
|
|
});
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case "use_back": {
|
|
// Return from use result to detail or list
|
|
if (!selectedItemId) break;
|
|
const entry = sorted.find(e => e.item.id === selectedItemId);
|
|
if (entry) {
|
|
await interaction.editReply(
|
|
getItemDetailMessage(entry, viewerId, ownerId)
|
|
);
|
|
} else {
|
|
selectedItemId = null;
|
|
if (sorted.length === 0) {
|
|
await interaction.editReply(getEmptyInventoryMessage(username));
|
|
} else {
|
|
await interaction.editReply(
|
|
getInventoryListMessage(sorted, username, currentPage, viewerId, ownerId)
|
|
);
|
|
}
|
|
}
|
|
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", async () => {
|
|
try {
|
|
// Re-render current view as static (no interactive components)
|
|
const entries = await inventoryService.getInventory(ownerId);
|
|
const sorted = sortInventoryItems(entries as InventoryEntry[]);
|
|
|
|
if (selectedItemId) {
|
|
const entry = sorted.find(e => e.item.id === selectedItemId);
|
|
if (entry) {
|
|
// Show detail view without action buttons
|
|
const msg = getItemDetailMessage(entry, viewerId, ownerId);
|
|
// Replace components with empty to remove buttons but keep container content
|
|
await interaction.editReply(msg);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (sorted.length === 0) {
|
|
await interaction.editReply(getEmptyInventoryMessage(username));
|
|
} else {
|
|
await interaction.editReply(
|
|
getInventoryListMessage(sorted, username, currentPage, viewerId, ownerId)
|
|
);
|
|
}
|
|
} catch {
|
|
// If re-rendering fails, at least try to clear gracefully
|
|
interaction.editReply({ components: [] }).catch(() => {});
|
|
}
|
|
});
|
|
}
|