diff --git a/src/commands/attack.ts b/src/commands/attack.ts index 462d877..60011c6 100644 --- a/src/commands/attack.ts +++ b/src/commands/attack.ts @@ -2,6 +2,7 @@ import { GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js import { weaponAutocomplete, getItem, getItemQuantity, formatItems, formatItem } from '../lib/rpg/items'; import { Command } from '../types/index'; import { initHealth, dealDamage, BLOOD_ITEM, BLOOD_ID, resetInvincible, INVINCIBLE_TIMER, getInvincibleMs } from '../lib/rpg/pvp'; +import { autocomplete } from '../lib/autocomplete'; export default { data: new SlashCommandBuilder() @@ -48,5 +49,5 @@ export default { await interaction.followUp(`You hit ${user} with ${formatItem(weapon)} for ${BLOOD_ITEM.emoji} **${dmg}** damage! They are now at ${formatItems(BLOOD_ITEM, newHealth.quantity)}.\nYou can attack them again (or if they perform an action first).`); }, - autocomplete: weaponAutocomplete, + autocomplete: autocomplete(weaponAutocomplete), } satisfies Command; \ No newline at end of file diff --git a/src/commands/counter.ts b/src/commands/counter.ts index e25a573..7167fc1 100644 --- a/src/commands/counter.ts +++ b/src/commands/counter.ts @@ -4,6 +4,7 @@ import { counterAutocomplete, counterConfigs, findCounter, getCounterConfigRaw, import { outdent } from 'outdent'; import { formatItem, formatItems, getItem, itemAutocomplete } from '../lib/rpg/items'; import { Command } from '../types/index'; +import { autocomplete, set } from '../lib/autocomplete'; function extendOption(t: string) { return {name: t, value: t}; @@ -442,24 +443,22 @@ export default { } }, - autocomplete: async (interaction: AutocompleteInteraction) => {{ - const focused = interaction.options.getFocused(true); + autocomplete: autocomplete(set({ + type: counterAutocomplete, + item: itemAutocomplete, + value: async (interaction: AutocompleteInteraction) => { + const focused = interaction.options.getFocused(); - if (focused.name === 'type') { - return counterAutocomplete(interaction); - } else if (focused.name === 'item') { - return itemAutocomplete(interaction); - } else if (focused.name === 'value') { const type = interaction.options.getString('type', true); const counter = await findCounter(type, interaction.guildId!); const config = await getCounterConfigRaw(counter); const key = interaction.options.getString('key'); - if (!key) return interaction.respond([]); + if (!key) return []; const defaultConfig = counterConfigs.get(key); - if (!defaultConfig) return interaction.respond([]); + if (!defaultConfig) return []; const defaultOptions = getOptions(defaultConfig.type); @@ -472,20 +471,20 @@ export default { value: `${toStringConfig(defaultConfig.default, defaultConfig.type)}`, name: `Default: ${toStringConfig(defaultConfig.default, defaultConfig.type)}` }, - ...defaultOptions.filter(s => s.startsWith(focused.value)).map(extendOption) + ...defaultOptions.filter(s => s.startsWith(focused)).map(extendOption) ]; - if (focused.value !== '' && !options.find(opt => opt.value === focused.value)) { + if (focused !== '' && !options.find(opt => opt.value === focused)) { options = [ { - value: focused.value, - name: focused.value + value: focused, + name: focused }, ...options ]; } - await interaction.respond(options); + return options; } - }} + })), } satisfies Command; \ No newline at end of file diff --git a/src/commands/craft.ts b/src/commands/craft.ts index a0d3d4f..fbfb061 100644 --- a/src/commands/craft.ts +++ b/src/commands/craft.ts @@ -5,6 +5,7 @@ import { formatItem, getItemQuantity, formatItems, getMaxStack, giveItem, format import { getRecipe, defaultRecipes, formatRecipe, resolveCustomRecipe } from '../lib/rpg/recipes'; import { Command } from '../types/index'; import { initHealth, resetInvincible } from '../lib/rpg/pvp'; +import { autocomplete, set } from '../lib/autocomplete'; export default { data: new SlashCommandBuilder() @@ -102,11 +103,9 @@ export default { return interaction.followUp(`${station.emoji} ${verb(station)} ${formatItemsArray(outputs)}!${outputs.length === 1 ? `\n_${outputs[0].item.description}_` : ''}${nextUsableAt ? `\n${station.name} usable again ` : ''}`); }, - autocomplete: async (interaction: AutocompleteInteraction) => { - const focused = interaction.options.getFocused(true); - - if (focused.name === 'station') { - const found = (await Promise.all( + autocomplete: autocomplete(set({ + station: async (interaction: AutocompleteInteraction) => + (await Promise.all( craftingStations .map(async station => [station, await canUseStation(interaction.user.id, station)]) )) @@ -114,19 +113,18 @@ export default { .filter(([_station, usable]) => usable) // eslint-disable-next-line @typescript-eslint/no-unused-vars .map(([station, _]) => station as CraftingStation) - .filter(station => station.name.toLowerCase().includes(focused.value.toLowerCase())) + .filter(station => station.name.toLowerCase().includes(interaction.options.getFocused().toLowerCase())) .map(station => ({ name: `${station.emoji} ${station.name}`, value: station.key - })); - - return interaction.respond(found); - } else if (focused.name === 'recipe') { + })), + 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.value.toLowerCase())).length > 0); + .filter(recipe => recipe.outputs.filter(n => n.item.name.toLowerCase().includes(focused.toLowerCase())).length > 0); const customRecipes = await db('customCraftingRecipes') .where('station', station); @@ -134,17 +132,15 @@ export default { const resolvedCustomRecipes = await Promise.all(customRecipes.map(resolveCustomRecipe)); const foundCustomRecipes = resolvedCustomRecipes - .filter(recipe => recipe.outputs.filter(n => n.item.name.toLowerCase().includes(focused.value.toLowerCase())).length > 0); + .filter(recipe => recipe.outputs.filter(n => n.item.name.toLowerCase().includes(focused.toLowerCase())).length > 0); const recipes = [...foundDefaultRecipes, ...foundCustomRecipes]; - return interaction.respond( - recipes - .map(recipe => ({ - name: formatRecipe(recipe, true), - value: recipe.id.toString() - })) - ); + return recipes + .map(recipe => ({ + name: formatRecipe(recipe, true), + value: recipe.id.toString() + })); } - } + })), } satisfies Command; \ No newline at end of file diff --git a/src/commands/decrease.ts b/src/commands/decrease.ts index 26edf6a..2be9adc 100644 --- a/src/commands/decrease.ts +++ b/src/commands/decrease.ts @@ -1,6 +1,7 @@ import { GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js'; import { changeCounterInteraction, counterAutocomplete } from '../lib/rpg/counter'; import { Command } from '../types/index'; +import { autocomplete } from '../lib/autocomplete' export default { data: new SlashCommandBuilder() @@ -35,5 +36,5 @@ export default { changeCounterInteraction(interaction, member, -amount, type); }, - autocomplete: counterAutocomplete + autocomplete: autocomplete(counterAutocomplete), } satisfies Command; \ No newline at end of file diff --git a/src/commands/increase.ts b/src/commands/increase.ts index 4a581c2..d060c7e 100644 --- a/src/commands/increase.ts +++ b/src/commands/increase.ts @@ -1,6 +1,7 @@ import { GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js'; import { changeCounterInteraction, counterAutocomplete } from '../lib/rpg/counter'; import { Command } from '../types/index'; +import { autocomplete } from '../lib/autocomplete'; export default { data: new SlashCommandBuilder() @@ -35,5 +36,5 @@ export default { changeCounterInteraction(interaction, member, amount, type); }, - autocomplete: counterAutocomplete + autocomplete: autocomplete(counterAutocomplete) } satisfies Command; \ No newline at end of file diff --git a/src/commands/item.ts b/src/commands/item.ts index 7f31f10..e449453 100644 --- a/src/commands/item.ts +++ b/src/commands/item.ts @@ -4,6 +4,7 @@ import { customItemAutocomplete, formatItem, formatItems, getCustomItem, getItem import { Command } from '../types/index'; import { formatRecipe, getCustomRecipe } from '../lib/rpg/recipes'; import { behaviors, formatBehavior, getBehavior } from '../lib/rpg/behaviors'; +import { autocomplete, set } from '../lib/autocomplete'; //function extendOption(t: string) { // return {name: t, value: t}; @@ -321,15 +322,12 @@ export default { } }, - autocomplete: async (interaction: AutocompleteInteraction) => { - const focused = interaction.options.getFocused(true); - - if (focused.name === 'item') { - return itemAutocomplete(interaction); - } else if (focused.name === 'customitem') { - return customItemAutocomplete(interaction); - } else if (focused.name === 'behavior') { - let foundBehaviors = behaviors.filter(b => b.name.toLowerCase().includes(focused.value.toLowerCase())); + autocomplete: autocomplete(set({ + item: itemAutocomplete, + customitem: customItemAutocomplete, + behavior: async (interaction: AutocompleteInteraction) => { + const focused = interaction.options.getFocused(); + let foundBehaviors = behaviors.filter(b => b.name.toLowerCase().includes(focused.toLowerCase())); const itemID = interaction.options.getString('customitem'); if (itemID) { const item = await getItem(parseInt(itemID)); @@ -338,22 +336,24 @@ export default { } } - await interaction.respond(foundBehaviors.map(b => ({name: `${b.itemType}:${b.name} - ${b.description}`, value: b.name}))); - } else if (focused.name === 'removebehavior') { + return foundBehaviors.map(b => ({name: `${b.itemType}:${b.name} - ${b.description}`, value: b.name})); + }, + removebehavior: async (interaction: AutocompleteInteraction) => { + const focused = interaction.options.getFocused(); const itemID = interaction.options.getString('customitem'); - if (!itemID) return await interaction.respond([]); + if (!itemID) return []; const behaviors = await db('itemBehaviors') .where('item', itemID); const foundBehaviors = behaviors .map(b => ({ behavior: getBehavior(b.behavior)!, value: b.value })) .filter(b => b.behavior) - .filter(b => b.behavior.name.toLowerCase().includes(focused.value.toLowerCase())); + .filter(b => b.behavior.name.toLowerCase().includes(focused.toLowerCase())); - await interaction.respond(foundBehaviors.map(b => ({ + return foundBehaviors.map(b => ({ name: `${b.behavior.itemType}:${formatBehavior(b.behavior, b.value)} - ${b.behavior.description}`, value: b.behavior.name - }))); - } - } + })); + }, + })), } satisfies Command; \ No newline at end of file diff --git a/src/commands/put.ts b/src/commands/put.ts index 0e02a3f..1b6888b 100644 --- a/src/commands/put.ts +++ b/src/commands/put.ts @@ -2,6 +2,7 @@ import { GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js import { changeLinkedCounterInteraction, linkedCounterAutocomplete } from '../lib/rpg/counter'; import { Command } from '../types/index'; import { initHealth } from '../lib/rpg/pvp'; +import { autocomplete } from '../lib/autocomplete'; export default { data: new SlashCommandBuilder() @@ -38,5 +39,5 @@ export default { changeLinkedCounterInteraction(interaction, member, amount, type); }, - autocomplete: linkedCounterAutocomplete + autocomplete: autocomplete(linkedCounterAutocomplete), } satisfies Command; \ No newline at end of file diff --git a/src/commands/recipe.ts b/src/commands/recipe.ts index 86b8dfe..893e70e 100644 --- a/src/commands/recipe.ts +++ b/src/commands/recipe.ts @@ -1,9 +1,10 @@ -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, ComponentType, Events, ModalBuilder, SlashCommandBuilder, StringSelectMenuBuilder, TextInputBuilder, TextInputStyle } from 'discord.js'; +import { ActionRowBuilder, AutocompleteInteraction, ButtonBuilder, ButtonStyle, CommandInteraction, ComponentType, Events, ModalBuilder, SlashCommandBuilder, StringSelectMenuBuilder, TextInputBuilder, TextInputStyle } from 'discord.js'; import { Command } from '../types/index'; import { Items, getItem } from '../lib/rpg/items'; import { formatRecipe, getCustomRecipe, resolveCustomRecipe } from '../lib/rpg/recipes'; import { craftingStations, getStation } from '../lib/rpg/craftingStations'; import { CustomCraftingRecipe, CustomCraftingRecipeItem, db } from '../lib/db'; +import { autocomplete } from '../lib/autocomplete'; export default { data: new SlashCommandBuilder() @@ -235,24 +236,20 @@ export default { }); }, - autocomplete: async (interaction) => { - const focused = interaction.options.getFocused(true); + autocomplete: autocomplete(async (interaction: AutocompleteInteraction) => { + const focused = interaction.options.getFocused(); - if (focused.name === 'recipe') { - const customRecipes = await db('customCraftingRecipes'); + const customRecipes = await db('customCraftingRecipes'); - const resolvedCustomRecipes = await Promise.all(customRecipes.map(resolveCustomRecipe)); + const resolvedCustomRecipes = await Promise.all(customRecipes.map(resolveCustomRecipe)); - const foundCustomRecipes = resolvedCustomRecipes - .filter(recipe => recipe.outputs.filter(n => n.item.name.toLowerCase().includes(focused.value.toLowerCase())).length > 0); + const foundCustomRecipes = resolvedCustomRecipes + .filter(recipe => recipe.outputs.filter(n => n.item.name.toLowerCase().includes(focused.toLowerCase())).length > 0); - return interaction.respond( - foundCustomRecipes - .map(recipe => ({ - name: formatRecipe(recipe, true), - value: recipe.id.toString() - })) - ); - } - } + return foundCustomRecipes + .map(recipe => ({ + name: formatRecipe(recipe, true), + value: recipe.id.toString() + })); + }), } satisfies Command; \ No newline at end of file diff --git a/src/commands/take.ts b/src/commands/take.ts index 31c22ce..bdba406 100644 --- a/src/commands/take.ts +++ b/src/commands/take.ts @@ -2,6 +2,7 @@ import { GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js import { changeLinkedCounterInteraction, linkedCounterAutocomplete } from '../lib/rpg/counter'; import { initHealth } from '../lib/rpg/pvp'; import { Command } from '../types/index'; +import { autocomplete } from '../lib/autocomplete'; export default { data: new SlashCommandBuilder() @@ -38,5 +39,5 @@ export default { changeLinkedCounterInteraction(interaction, member, -amount, type); }, - autocomplete: linkedCounterAutocomplete + autocomplete: autocomplete(linkedCounterAutocomplete) } satisfies Command; \ No newline at end of file diff --git a/src/lib/autocomplete.ts b/src/lib/autocomplete.ts new file mode 100644 index 0000000..23af2c3 --- /dev/null +++ b/src/lib/autocomplete.ts @@ -0,0 +1,27 @@ +import { AutocompleteInteraction, ApplicationCommandOptionChoiceData } from 'discord.js'; +import * as log from './log'; + +export type Autocomplete = (interaction: AutocompleteInteraction) => Promise[]> + +export function set(fns: Record): Autocomplete { + return async (interaction: AutocompleteInteraction) => { + const focused = interaction.options.getFocused(true); + const fn = fns[focused.name]; + + if (!fn) return []; + + return fn(interaction); + }; +} +export function autocomplete(fn: Autocomplete): (interaction: AutocompleteInteraction) => Promise { + return async (interaction: AutocompleteInteraction) => { + try { + const arr = await fn(interaction); + if (arr.length > 25) log.warn(`Autocomplete for ${interaction.options.getFocused(true).name} exceeded limit of 25 autocomplete results`); + return interaction.respond(arr.slice(0, 25)); + } catch (err) { + log.error(err); + return interaction.respond([]); + } + }; +} \ No newline at end of file diff --git a/src/lib/rpg/counter.ts b/src/lib/rpg/counter.ts index 9ec814f..2ea47d8 100644 --- a/src/lib/rpg/counter.ts +++ b/src/lib/rpg/counter.ts @@ -3,6 +3,7 @@ import { getSign } from '../util'; import { Counter, CounterConfiguration, CounterUserLink, db } from '../db'; import { formatItems, getItem, getItemQuantity, getMaxStack, giveItem } from './items'; import { resetInvincible } from './pvp'; +import { Autocomplete } from '../autocomplete'; export async function getCounter(id: number) { const counter = await db('counters') @@ -378,7 +379,7 @@ function changeCounterInteractionBuilder(linked: boolean) { export const changeCounterInteraction = changeCounterInteractionBuilder(false); export const changeLinkedCounterInteraction = changeCounterInteractionBuilder(true); -function counterAutocompleteBuilder(linked: boolean) { +function counterAutocompleteBuilder(linked: boolean): Autocomplete { return async (interaction: AutocompleteInteraction) => { const focusedValue = interaction.options.getFocused(); const guild = interaction.guildId; @@ -397,9 +398,7 @@ function counterAutocompleteBuilder(linked: boolean) { const foundCounters = await query; - await interaction.respond( - foundCounters.map(choice => ({ name: `${choice.emoji} ${choice.key}`, value: choice.key })) - ); + return foundCounters.map(choice => ({ name: `${choice.emoji} ${choice.key}`, value: choice.key })); }; } diff --git a/src/lib/rpg/items.ts b/src/lib/rpg/items.ts index 19d49ad..56a4f56 100644 --- a/src/lib/rpg/items.ts +++ b/src/lib/rpg/items.ts @@ -1,5 +1,6 @@ import { AutocompleteInteraction } from 'discord.js'; import { CustomItem, ItemBehavior, ItemInventory, db } from '../db'; +import { Autocomplete } from '../autocomplete'; // uses negative IDs export type DefaultItem = Omit & { behaviors?: Omit[] }; @@ -378,7 +379,7 @@ export function formatItemsArray(items: Items[], disableBold = false) { return items.map(i => formatItems(i.item, i.quantity, disableBold)).join(' '); } -function createItemAutocomplete(onlyCustom: boolean, filterType: 'plain' | 'weapon' | 'consumable' | null) { +function createItemAutocomplete(onlyCustom: boolean, filterType: 'plain' | 'weapon' | 'consumable' | null): Autocomplete { return async (interaction: AutocompleteInteraction) => { const focused = interaction.options.getFocused(); @@ -403,9 +404,7 @@ function createItemAutocomplete(onlyCustom: boolean, filterType: 'plain' | 'weap items = [...foundDefaultItems, ...customItems]; } - await interaction.respond( - items.map(choice => ({ name: `${choice.emoji} ${choice.name}`, value: choice.id.toString() })) - ); + return items.map(choice => ({ name: `${choice.emoji} ${choice.name}`, value: choice.id.toString() })); }; }