import { AutocompleteInteraction, GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js'; import { CraftingStationCooldown, CustomCraftingRecipe, db } from '../lib/db'; import { getStation, canUseStation, craftingStations, verb, CraftingStation } from '../lib/rpg/craftingStations'; import { formatItem, getItemQuantity, formatItems, getMaxStack, giveItem, formatItemsArray } from '../lib/rpg/items'; import { getRecipe, defaultRecipes, formatRecipe, resolveCustomRecipe } from '../lib/rpg/recipes'; import { Command } from '../types/index'; import { initHealth, resetInvincible } from '../lib/rpg/pvp'; import { set } from '../lib/autocomplete'; export default { data: new SlashCommandBuilder() .setName('craft') .setDescription('Craft an item with items you have') .addStringOption(option => option .setName('station') .setAutocomplete(true) .setDescription('Which station to use') .setRequired(true) ) .addStringOption(option => option .setName('recipe') .setAutocomplete(true) .setDescription('What to craft') .setRequired(true) ) .setDMPermission(false), execute: async (interaction: CommandInteraction) => { if (!interaction.isChatInputCommand()) return; const member = interaction.member! as GuildMember; await initHealth(member.id); const recipeID = parseInt(interaction.options.getString('recipe', true)); await interaction.deferReply({ephemeral: true}); const recipe = await getRecipe(recipeID); if (!recipe) return interaction.followUp('Recipe does not exist!'); const station = getStation(recipe.station)!; if (!canUseStation(member.id, station)) return interaction.followUp(`${station.emoji} You need ${formatItem(station.requires)} to use this station!`); for (const input of recipe.inputs) { const inv = await getItemQuantity(member.id, input.item.id); if (inv.quantity < input.quantity) return interaction.followUp(`You need ${formatItems(input.item, input.quantity)} for this recipe! (You have ${formatItems(input.item, inv.quantity)})`); } for (const req of recipe.requirements) { const inv = await getItemQuantity(member.id, req.item.id); if (inv.quantity < req.quantity) return interaction.followUp(`You need ${formatItems(req.item, req.quantity)} to begin this recipe! (You have ${formatItems(req.item, inv.quantity)}. Don't worry, these items will not be consumed.)`); } for (const out of recipe.outputs) { const inv = await getItemQuantity(member.id, out.item.id); if (inv.quantity + out.quantity > getMaxStack(out.item)) return interaction.followUp(`You do not have enough inventory storage for this recipe! (${formatItems(out.item, inv.quantity + out.quantity)} is bigger than the stack size of ${getMaxStack(out.item)}x.)`); } let cooldown; if (station.cooldown) { cooldown = await db('craftingStationCooldowns') .where('station', station.key) .where('user', member.id) .first(); if (cooldown && (cooldown.usedAt + station.cooldown * 1000) > Date.now()) return interaction.followUp(`${station.emoji} You can use this station again !`); } // proceed with crafting! for (const input of recipe.inputs) { giveItem(member.id, input.item, -input.quantity); } const outputs = station.manipulateResults ? station.manipulateResults(recipe.outputs) : recipe.outputs; for (const output of outputs) { giveItem(member.id, output.item, output.quantity); } let nextUsableAt; if (station.cooldown) { if (!cooldown) { await db('craftingStationCooldowns') .insert({ station: station.key, user: member.id, usedAt: Date.now() }); } else { await db('craftingStationCooldowns') .where('station', station.key) .where('user', member.id) .update({ usedAt: Date.now() }); } nextUsableAt = Date.now() + station.cooldown * 1000; } await resetInvincible(member.id); return interaction.followUp(`${station.emoji} ${verb(station)} ${formatItemsArray(outputs)}!${outputs.length === 1 ? `\n_${outputs[0].item.description}_` : ''}${nextUsableAt ? `\n${station.name} usable again ` : ''}`); }, autocomplete: set({ station: async (interaction: AutocompleteInteraction) => (await Promise.all( craftingStations .map(async station => [station, await canUseStation(interaction.user.id, station)]) )) // eslint-disable-next-line @typescript-eslint/no-unused-vars .filter(([_station, usable]) => usable) // eslint-disable-next-line @typescript-eslint/no-unused-vars .map(([station, _]) => station as CraftingStation) .filter(station => station.name.toLowerCase().includes(interaction.options.getFocused().toLowerCase())) .map(station => ({ name: `${station.emoji} ${station.name}`, value: station.key })), recipe: async (interaction: AutocompleteInteraction) => { const focused = interaction.options.getFocused(); const station = interaction.options.getString('station'); const foundDefaultRecipes = defaultRecipes .filter(recipe => recipe.station === station) .filter(recipe => recipe.outputs.filter(n => n.item.name.toLowerCase().includes(focused.toLowerCase())).length > 0); const customRecipes = await db('customCraftingRecipes') .where('station', station); const resolvedCustomRecipes = await Promise.all(customRecipes.map(resolveCustomRecipe)); const foundCustomRecipes = resolvedCustomRecipes .filter(recipe => recipe.outputs.filter(n => n.item.name.toLowerCase().includes(focused.toLowerCase())).length > 0); const recipes = [...foundDefaultRecipes, ...foundCustomRecipes]; return recipes .map(recipe => ({ name: formatRecipe(recipe, true), value: recipe.id.toString() })); } }), } satisfies Command;