jillo-bot/src/commands/craft.ts

147 lines
6.2 KiB
TypeScript

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<CraftingStationCooldown>('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 <t:${Math.floor(cooldown.usedAt / 1000 + station.cooldown)}:R>!`);
}
// 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<CraftingStationCooldown>('craftingStationCooldowns')
.insert({
station: station.key,
user: member.id,
usedAt: Date.now()
});
} else {
await db<CraftingStationCooldown>('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 <t:${Math.floor(nextUsableAt / 1000)}:R>` : ''}`);
},
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<CustomCraftingRecipe>('customCraftingRecipes')
.where('station', station)
.where('guild', interaction.guildId);
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;