linked counters

This commit is contained in:
Jill 2023-11-15 13:03:01 +03:00
parent 4351e6dfad
commit a0c9b12da0
Signed by: oat
GPG Key ID: 33489AA58A955108
8 changed files with 353 additions and 128 deletions

View File

@ -0,0 +1,21 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
return knex.schema
.alterTable('counters', table => {
table.integer('linkedItem');
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
return knex.schema
.alterTable('counters', table => {
table.dropColumn('linkedItem');
});
};

View File

@ -2,6 +2,7 @@ import { AutocompleteInteraction, Interaction, SlashCommandBuilder } from 'disco
import { Counter, CounterUserLink, db } from '../lib/db'; import { Counter, CounterUserLink, db } from '../lib/db';
import { counterAutocomplete, counterConfigs, findCounter, getCounterConfigRaw, getOptions, parseConfig, setCounterConfig, toStringConfig, updateCounter } from '../lib/counter'; import { counterAutocomplete, counterConfigs, findCounter, getCounterConfigRaw, getOptions, parseConfig, setCounterConfig, toStringConfig, updateCounter } from '../lib/counter';
import { outdent } from 'outdent'; import { outdent } from 'outdent';
import { formatItem, formatItems, getItem, itemAutocomplete } from '../lib/items';
function extendOption(t: string) { function extendOption(t: string) {
return {name: t, value: t}; return {name: t, value: t};
@ -9,7 +10,7 @@ function extendOption(t: string) {
const help = new Map([ const help = new Map([
['message templates', outdent` ['message templates', outdent`
When using \`messageTemplate\`, \`messageTemplateIncrease\` or \`messageTemplateDecrease\`, you are providing a **template string**. When using \`messageTemplate\`, \`messageTemplateIncrease\`, \`messageTemplateDecrease\`, \`messageTemplatePut\` or \`messageTemplateTake\`, you are providing a **template string**.
A template string is a **specially-formatted** string with placeholder values. For instance, a template string like so: A template string is a **specially-formatted** string with placeholder values. For instance, a template string like so:
> **%user** has %action the counter by **%amt**. > **%user** has %action the counter by **%amt**.
@ -215,6 +216,25 @@ module.exports = {
.setChoices(...[...help.keys()].map(extendOption)) .setChoices(...[...help.keys()].map(extendOption))
) )
) )
.addSubcommand(sub =>
sub
.setName('link')
.setDescription('[ADMIN] THIS IS IRREVERSIBLE! Attach an item to this counter, letting you take or put items in.')
.addStringOption(opt =>
opt
.setName('type')
.setDescription('The counter to operate on')
.setRequired(true)
.setAutocomplete(true)
)
.addStringOption(opt =>
opt
.setName('item')
.setDescription('The item')
.setAutocomplete(true)
.setRequired(true)
)
)
.setDefaultMemberPermissions('0') .setDefaultMemberPermissions('0')
.setDMPermission(false), .setDMPermission(false),
@ -233,10 +253,7 @@ module.exports = {
try { try {
counter = await findCounter(type, interaction.guildId!); counter = await findCounter(type, interaction.guildId!);
} catch(err) { } catch(err) {
await interaction.followUp({ return interaction.followUp('No such counter!');
content: 'No such counter!'
});
return;
} }
if (subcommand === 'add') { if (subcommand === 'add') {
@ -276,12 +293,7 @@ module.exports = {
.where('producer', userType === 'producer') .where('producer', userType === 'producer')
.first(); .first();
if (!link) { if (!link) return interaction.followUp(`<@${user.id}> is not in the ${counter.emoji} **${userType}** allowlist!`);
await interaction.followUp({
content: `<@${user.id}> is not in the ${counter.emoji} **${userType}** allowlist!`
});
return;
}
await interaction.followUp({ await interaction.followUp({
content: `<@${user.id}> has been removed from the ${counter.emoji} **${userType}** allowlist.` content: `<@${user.id}> has been removed from the ${counter.emoji} **${userType}** allowlist.`
@ -350,15 +362,14 @@ module.exports = {
try { try {
counter = await findCounter(type, interaction.guildId!); counter = await findCounter(type, interaction.guildId!);
} catch(err) { } catch(err) {
await interaction.followUp({ return interaction.followUp('No such counter!');
content: 'No such counter!'
});
return;
} }
const config = await getCounterConfigRaw(counter); const config = await getCounterConfigRaw(counter);
const key = interaction.options.getString('key', true); const key = interaction.options.getString('key', true);
const value = interaction.options.getString('value', true); const value = interaction.options.getString('value', true);
if (key === 'emoji' && counter.linkedItem) return interaction.followUp(`Cannot modify emoji - this counter is linked to ${formatItem(await getItem(counter.linkedItem))}`);
const defaultConfig = counterConfigs.get(key); const defaultConfig = counterConfigs.get(key);
if (!defaultConfig) return interaction.followUp(`No config named \`${key}\` exists!`); if (!defaultConfig) return interaction.followUp(`No config named \`${key}\` exists!`);
@ -366,7 +377,7 @@ module.exports = {
const parsedValue = parseConfig(value, defaultConfig.type); const parsedValue = parseConfig(value, defaultConfig.type);
const restringedValue = toStringConfig(parsedValue, defaultConfig.type); const restringedValue = toStringConfig(parsedValue, defaultConfig.type);
await setCounterConfig(counter.id, key, restringedValue); await setCounterConfig(counter, key, restringedValue);
await interaction.followUp(`${counter.emoji} \`${key}\` is now \`${restringedValue}\`. (was \`${config.get(key) || toStringConfig(defaultConfig.default, defaultConfig.type)}\`)`); await interaction.followUp(`${counter.emoji} \`${key}\` is now \`${restringedValue}\`. (was \`${config.get(key) || toStringConfig(defaultConfig.default, defaultConfig.type)}\`)`);
} else if (subcommand === 'delete') { } else if (subcommand === 'delete') {
@ -376,10 +387,7 @@ module.exports = {
try { try {
counter = await findCounter(type, interaction.guildId!); counter = await findCounter(type, interaction.guildId!);
} catch(err) { } catch(err) {
await interaction.followUp({ return interaction.followUp('No such counter!');
content: 'No such counter!'
});
return;
} }
await db<Counter>('counters') await db<Counter>('counters')
@ -400,6 +408,32 @@ module.exports = {
await interaction.followUp(counters.map(c => `${c.emoji} **${c.value}** <#${c.channel}>`).join('\n')); await interaction.followUp(counters.map(c => `${c.emoji} **${c.value}** <#${c.channel}>`).join('\n'));
} else if (subcommand === 'help') { } else if (subcommand === 'help') {
await interaction.followUp(help.get(interaction.options.getString('topic', true))!); await interaction.followUp(help.get(interaction.options.getString('topic', true))!);
} else if (subcommand === 'link') {
const type = interaction.options.getString('type', true);
const itemID = parseInt(interaction.options.getString('item', true));
let counter;
try {
counter = await findCounter(type, interaction.guildId!);
} catch(err) {
return interaction.followUp('No such counter!');
}
const item = await getItem(itemID);
if (!item) return interaction.followUp('No such item exists!');
await db<Counter>('counters')
.where('id', counter.id)
.update({
'linkedItem': item.id,
'emoji': item.emoji,
'value': 0
});
await setCounterConfig(counter, 'canIncrement', 'false');
await setCounterConfig(counter, 'canDecrement', 'false');
await interaction.followUp(`Done. **The counter has been reset** to ${formatItems(item, 0)}. Users will not be able to take out or put in items until you enable this with \`canTake\` or \`canPut\`.\n\`canIncrement\` and \`canDecrement\` have also been **automatically disabled**, and you are recommended to keep them as such if you want to maintain balance in the universe.`);
} }
} }
}, },
@ -409,6 +443,8 @@ module.exports = {
if (focused.name === 'type') { if (focused.name === 'type') {
return counterAutocomplete(interaction); return counterAutocomplete(interaction);
} else if (focused.name === 'item') {
return itemAutocomplete(interaction);
} else if (focused.name === 'value') { } else if (focused.name === 'value') {
const type = interaction.options.getString('type', true); const type = interaction.options.getString('type', true);
const counter = await findCounter(type, interaction.guildId!); const counter = await findCounter(type, interaction.guildId!);

View File

@ -1,6 +1,6 @@
import { AutocompleteInteraction, Interaction, SlashCommandBuilder } from 'discord.js'; import { AutocompleteInteraction, Interaction, SlashCommandBuilder } from 'discord.js';
import { CustomItem, ItemInventory, db } from '../lib/db'; import { CustomItem, ItemInventory, db } from '../lib/db';
import { behaviors, defaultItems, formatItems, getItem, getMaxStack } from '../lib/items'; import { behaviors, formatItems, getItem, getMaxStack, giveItem, itemAutocomplete } from '../lib/items';
//function extendOption(t: string) { //function extendOption(t: string) {
// return {name: t, value: t}; // return {name: t, value: t};
@ -204,32 +204,9 @@ module.exports = {
const item = await getItem(itemID); const item = await getItem(itemID);
if (!item) return interaction.followUp('No such item exists!'); if (!item) return interaction.followUp('No such item exists!');
const storedItem = await db<ItemInventory>('itemInventories') const inv = await giveItem(user.id, item, quantity);
.where('user', user.id)
.where('item', itemID)
.first();
let inv; await interaction.followUp(`${user.toString()} now has ${formatItems(item, inv.quantity)}.`);
if (storedItem) {
inv = await db<ItemInventory>('itemInventories')
.update({
'quantity': db.raw('MIN(quantity + ?, ?)', [quantity, getMaxStack(item)])
})
.limit(1)
.where('user', user.id)
.where('item', itemID)
.returning('*');
} else {
inv = await db<ItemInventory>('itemInventories')
.insert({
'user': user.id,
'item': Math.min(itemID, getMaxStack(item)),
'quantity': quantity
})
.returning('*');
}
await interaction.followUp(`${user.toString()} now has ${formatItems(item, inv[0].quantity)}.`);
} }
} }
}, },
@ -238,20 +215,7 @@ module.exports = {
const focused = interaction.options.getFocused(true); const focused = interaction.options.getFocused(true);
if (focused.name === 'item') { if (focused.name === 'item') {
const customItems = await db<CustomItem>('customItems') return itemAutocomplete(interaction);
.select('emoji', 'name', 'id')
// @ts-expect-error this LITERALLY works
.whereLike(db.raw('UPPER(name)'), `%${focused.value.toUpperCase()}%`)
.where('guild', interaction.guildId!)
.limit(25);
const foundDefaultItems = defaultItems.filter(item => item.name.toUpperCase().includes(focused.value.toUpperCase()));
const items = [...foundDefaultItems, ...customItems];
await interaction.respond(
items.map(choice => ({ name: `${choice.emoji} ${choice.name}`, value: choice.id.toString() }))
);
} }
} }
}; };

36
src/commands/put.ts Normal file
View File

@ -0,0 +1,36 @@
import { GuildMember, Interaction, SlashCommandBuilder } from 'discord.js';
import { changeLinkedCounterInteraction, linkedCounterAutocomplete } from '../lib/counter';
module.exports = {
data: new SlashCommandBuilder()
.setName('put')
.setDescription('Put an item from your inventory into the counter')
.addStringOption(option =>
option
.setName('type')
.setAutocomplete(true)
.setDescription('The name of the counter')
.setRequired(true)
)
.addIntegerOption((option) =>
option
.setName('amount')
.setRequired(false)
.setDescription('Amount of items to put in')
.setMinValue(1)
)
.setDMPermission(false),
execute: async (interaction: Interaction, member: GuildMember) => {
if (!interaction.isChatInputCommand()) return;
const amount = Math.trunc(interaction.options.getInteger('amount') || 1);
const type = interaction.options.getString('type')!;
await interaction.deferReply({ephemeral: true});
changeLinkedCounterInteraction(interaction, member, amount, type);
},
autocomplete: linkedCounterAutocomplete
};

36
src/commands/take.ts Normal file
View File

@ -0,0 +1,36 @@
import { GuildMember, Interaction, SlashCommandBuilder } from 'discord.js';
import { changeLinkedCounterInteraction, linkedCounterAutocomplete } from '../lib/counter';
module.exports = {
data: new SlashCommandBuilder()
.setName('take')
.setDescription('Take an item from a counter')
.addStringOption(option =>
option
.setName('type')
.setAutocomplete(true)
.setDescription('The name of the counter')
.setRequired(true)
)
.addIntegerOption((option) =>
option
.setName('amount')
.setRequired(false)
.setDescription('Amount of items to take')
.setMinValue(1)
)
.setDMPermission(false),
execute: async (interaction: Interaction, member: GuildMember) => {
if (!interaction.isChatInputCommand()) return;
const amount = Math.trunc(interaction.options.getInteger('amount') || 1);
const type = interaction.options.getString('type')!;
await interaction.deferReply({ephemeral: true});
changeLinkedCounterInteraction(interaction, member, -amount, type);
},
autocomplete: linkedCounterAutocomplete
};

View File

@ -1,6 +1,7 @@
import { Client, CommandInteraction, GuildMember, EmbedBuilder, TextChannel, AutocompleteInteraction } from 'discord.js'; import { Client, CommandInteraction, GuildMember, EmbedBuilder, TextChannel, AutocompleteInteraction, User } from 'discord.js';
import { getSign } from './util'; import { getSign } from './util';
import { Counter, CounterConfiguration, CounterUserLink, db } from './db'; import { Counter, CounterConfiguration, CounterUserLink, db } from './db';
import { formatItems, getItem, getItemQuantity, getMaxStack, giveItem } from './items';
export async function getCounter(id: number) { export async function getCounter(id: number) {
const counter = await db<Counter>('counters') const counter = await db<Counter>('counters')
@ -83,11 +84,11 @@ export async function getCounterConfig(id: number, key: string) {
return value; return value;
} }
export async function setCounterConfig(id: number, option: string, value: string) { export async function setCounterConfig(counter: Counter, option: string, value: string) {
// just the ugly way of life // just the ugly way of life
if (option === 'emoji') { if (option === 'emoji' && !counter.linkedItem) {
await db<Counter>('counters') await db<Counter>('counters')
.where('id', id) .where('id', counter.id)
.update({ .update({
'emoji': value 'emoji': value
}); });
@ -98,13 +99,13 @@ export async function setCounterConfig(id: number, option: string, value: string
.update({ .update({
value: value value: value
}) })
.where('id', id) .where('id', counter.id)
.where('configName', option); .where('configName', option);
if (updated === 0) { if (updated === 0) {
await db<CounterConfiguration>('counterConfigurations') await db<CounterConfiguration>('counterConfigurations')
.insert({ .insert({
'id': id, 'id': counter.id,
'configName': option, 'configName': option,
'value': value 'value': value
}); });
@ -174,6 +175,30 @@ export const counterConfigs = new Map([
type: ConfigType.String, type: ConfigType.String,
default: 'null' default: 'null'
}], }],
['messageTemplateTake', {
type: ConfigType.String,
default: '**%user** has taken **%amt** from the counter.'
}],
['messageTemplatePut', {
type: ConfigType.String,
default: '**%user** has put **%amt** into the counter.'
}],
['canIncrement', {
type: ConfigType.Bool,
default: true
}],
['canDecrement', {
type: ConfigType.Bool,
default: true
}],
['canPut', {
type: ConfigType.Bool,
default: false
}],
['canTake', {
type: ConfigType.Bool,
default: false
}],
// these ones are fake and are just stand-ins for values defined inside the actual counters table // these ones are fake and are just stand-ins for values defined inside the actual counters table
['emoji', { ['emoji', {
@ -210,7 +235,7 @@ export async function updateCounter(bot: Client, counter: Counter, value: number
} }
} }
export async function announceCounterUpdate(bot: Client, member: GuildMember, delta: number, counter: Counter, value: number) { export async function announceCounterUpdate(bot: Client, member: GuildMember, delta: number, counter: Counter, value: number, linked: boolean = false) {
const channel = await bot.channels.fetch(counter.channel) as TextChannel; const channel = await bot.channels.fetch(counter.channel) as TextChannel;
let template = await getCounterConfig(counter.id, 'messageTemplate') as string; let template = await getCounterConfig(counter.id, 'messageTemplate') as string;
@ -218,6 +243,10 @@ export async function announceCounterUpdate(bot: Client, member: GuildMember, de
if (templateIncrease !== 'null' && delta > 0) template = templateIncrease; if (templateIncrease !== 'null' && delta > 0) template = templateIncrease;
const templateDecrease = await getCounterConfig(counter.id, 'messageTemplateDecrease') as string; const templateDecrease = await getCounterConfig(counter.id, 'messageTemplateDecrease') as string;
if (templateDecrease !== 'null' && delta < 0) template = templateDecrease; if (templateDecrease !== 'null' && delta < 0) template = templateDecrease;
const templatePut = await getCounterConfig(counter.id, 'messageTemplatePut') as string;
if (templatePut !== 'null' && delta > 0 && linked) template = templatePut;
const templateTake = await getCounterConfig(counter.id, 'messageTemplateTake') as string;
if (templateTake !== 'null' && delta < 0 && linked) template = templateTake;
const anonymous = await getCounterConfig(counter.id, 'anonymous') as boolean; const anonymous = await getCounterConfig(counter.id, 'anonymous') as boolean;
@ -226,7 +255,7 @@ export async function announceCounterUpdate(bot: Client, member: GuildMember, de
.setDescription( .setDescription(
template template
.replaceAll('%user', anonymous ? 'someone' : member.toString()) .replaceAll('%user', anonymous ? 'someone' : member.toString())
.replaceAll('%action', delta > 0 ? 'increased' : 'decreased') .replaceAll('%action', delta > 0 ? (linked ? 'put into' : 'increased') : (linked ? 'taken from' : 'decreased'))
.replaceAll('%amt', Math.abs(delta).toString()) .replaceAll('%amt', Math.abs(delta).toString())
.replaceAll('%total', value.toString()) .replaceAll('%total', value.toString())
) )
@ -249,66 +278,107 @@ export async function announceCounterUpdate(bot: Client, member: GuildMember, de
}); });
} }
export async function changeCounterInteraction(interaction: CommandInteraction, member: GuildMember, amount: number, type: string) { async function canUseCounter(user: User, counter: Counter, amount: number, isLinkedAction = false): Promise<boolean> {
try { if (amount > 0 && !(await getCounterConfig(counter.id, isLinkedAction ? 'canPut' : 'canIncrement') as boolean)) return false;
const counter = await findCounter(type, member.guild.id); if (amount > 0 && counter.allowlistProducer) {
const userLink = await db<CounterUserLink>('counterUserLink')
.where('id', counter.id)
.where('user', user.id)
.where('producer', true)
.first();
let canUse = true; if (!userLink) return false;
if (amount > 0 && counter.allowlistProducer) {
const userLink = await db<CounterUserLink>('counterUserLink')
.where('id', counter.id)
.where('user', member.id)
.where('producer', true)
.first();
if (!userLink) canUse = false;
}
if (amount < 0 && counter.allowlistConsumer) {
const userLink = await db<CounterUserLink>('counterUserLink')
.where('id', counter.id)
.where('user', member.id)
.where('producer', false)
.first();
if (!userLink) canUse = false;
}
if (!canUse) {
await interaction.followUp({
content: `You cannot **${amount > 0 ? 'produce' : 'consume'}** ${counter.emoji}.`
});
return;
}
const newCount = await changeCounter(counter.id, amount);
await updateCounter(interaction.client, counter, newCount);
await announceCounterUpdate(interaction.client, member, amount, counter, newCount);
await interaction.followUp({
content: `${counter.emoji} **You have ${amount > 0 ? 'increased' : 'decreased'} the counter.**\n\`\`\`diff\n ${newCount - amount}\n${getSign(amount)}${Math.abs(amount)}\n ${newCount}\`\`\``
});
} catch(err) {
await interaction.followUp({
content: (err as Error).toString()
});
} }
if (amount < 0 && !(await getCounterConfig(counter.id, isLinkedAction ? 'canTake' : 'canDecrement') as boolean)) return false;
if (amount < 0 && counter.allowlistConsumer) {
const userLink = await db<CounterUserLink>('counterUserLink')
.where('id', counter.id)
.where('user', user.id)
.where('producer', false)
.first();
if (!userLink) return false;
}
return true;
} }
export async function counterAutocomplete(interaction: AutocompleteInteraction) { function changeCounterInteractionBuilder(linked: boolean) {
const focusedValue = interaction.options.getFocused(); return async (interaction: CommandInteraction, member: GuildMember, amount: number, type: string) => {
const guild = interaction.guildId; try {
const counter = await findCounter(type, member.guild.id);
if (linked && !counter.linkedItem) return interaction.followUp('There is no such linked counter!');
const query = db<Counter>('counters') const canUse = await canUseCounter(member.user, counter, amount, linked);
.select('emoji', 'key')
.whereLike('key', `%${focusedValue.toLowerCase()}%`)
.limit(25);
if (guild) { if (!canUse) {
query.where('guild', guild); return interaction.followUp(`You cannot **${amount > 0 ? (linked ? 'put' : 'produce') : (linked ? 'take' : 'consume')}** ${counter.emoji}.`);
} }
const foundCounters = await query; let item;
let newInv;
if (linked) {
const inv = await getItemQuantity(member.id, counter.linkedItem!);
item = (await getItem(counter.linkedItem!))!;
await interaction.respond( // change counter by -10 = increment own counter by 10
foundCounters.map(choice => ({ name: choice.emoji, value: choice.key })) const amtInv = -amount;
); const amtAbs = Math.abs(amtInv);
}
if (amtInv > getMaxStack(item)) {
return interaction.followUp(`You cannot take ${formatItems(item, amtAbs)}, because the max stack size is ${getMaxStack(item)}x!`);
}
if ((inv.quantity + amtInv) > getMaxStack(item)) {
return interaction.followUp(`You cannot take ${formatItems(item, amtAbs)}, because the max stack size is ${getMaxStack(item)}x and you already have ${inv.quantity}x!`);
}
if ((inv.quantity + amtInv) < 0) {
return interaction.followUp(`You cannot put in ${formatItems(item, amtAbs)}, as you only have ${formatItems(item, inv.quantity)}!`);
}
newInv = await giveItem(member.id, item, amtInv);
}
const newCount = await changeCounter(counter.id, amount);
await updateCounter(interaction.client, counter, newCount);
await announceCounterUpdate(interaction.client, member, amount, counter, newCount, linked);
await interaction.followUp({
content: `${counter.emoji} **You have ${amount > 0 ? (linked ? 'put into' : 'increased') : (linked ? 'taken from' : 'decreased')} the counter.**\n\`\`\`diff\n ${newCount - amount}\n${getSign(amount)}${Math.abs(amount)}\n ${newCount}\`\`\`${newInv ? `\nYou now have ${formatItems(item, newInv.quantity)}.` : ''}`
});
} catch(err) {
await interaction.followUp({
content: (err as Error).toString()
});
}
};
}
export const changeCounterInteraction = changeCounterInteractionBuilder(false);
export const changeLinkedCounterInteraction = changeCounterInteractionBuilder(true);
function counterAutocompleteBuilder(linked: boolean) {
return async (interaction: AutocompleteInteraction) => {
const focusedValue = interaction.options.getFocused();
const guild = interaction.guildId;
const query = db<Counter>('counters')
.select('emoji', 'key')
.whereLike('key', `%${focusedValue.toLowerCase()}%`)
.limit(25);
if (guild) {
query.where('guild', guild);
}
if (linked) {
query.whereNotNull('linkedItem');
}
const foundCounters = await query;
await interaction.respond(
foundCounters.map(choice => ({ name: choice.emoji, value: choice.key }))
);
};
}
export const counterAutocomplete = counterAutocompleteBuilder(false);
export const linkedCounterAutocomplete = counterAutocompleteBuilder(true);

View File

@ -33,7 +33,8 @@ export interface Counter {
guild: string, guild: string,
message?: string, message?: string,
allowlistConsumer: boolean, allowlistConsumer: boolean,
allowlistProducer: boolean allowlistProducer: boolean,
linkedItem?: number
} }
export interface CounterUserLink { export interface CounterUserLink {
id: number, id: number,

View File

@ -1,5 +1,5 @@
import { User } from 'discord.js'; import { AutocompleteInteraction, User } from 'discord.js';
import { CustomItem, db } from './db'; import { CustomItem, ItemInventory, db } from './db';
type DefaultItem = Omit<CustomItem, 'guild'>; // uses negative IDs type DefaultItem = Omit<CustomItem, 'guild'>; // uses negative IDs
type Item = DefaultItem | CustomItem; type Item = DefaultItem | CustomItem;
@ -53,13 +53,74 @@ export async function getItem(id: number): Promise<Item | undefined> {
} }
} }
export async function getItemQuantity(user: string, itemID: number) {
return (await db<ItemInventory>('itemInventories')
.where('item', itemID)
.where('user', user)
.first())
|| {
'user': user,
'item': itemID,
'quantity': 0
};
}
export async function giveItem(user: string, item: Item, quantity = 1) {
const storedItem = await db<ItemInventory>('itemInventories')
.where('user', user)
.where('item', item.id)
.first();
let inv;
if (storedItem) {
inv = await db<ItemInventory>('itemInventories')
.update({
'quantity': db.raw('MIN(quantity + ?, ?)', [quantity, getMaxStack(item)])
})
.limit(1)
.where('user', user)
.where('item', item.id)
.returning('*');
} else {
inv = await db<ItemInventory>('itemInventories')
.insert({
'user': user,
'item': Math.min(item.id, getMaxStack(item)),
'quantity': quantity
})
.returning('*');
}
return inv[0];
}
export function getMaxStack(item: Item) { export function getMaxStack(item: Item) {
return item.type === 'weapon' ? 1 : item.maxStack; return item.type === 'weapon' ? 1 : item.maxStack;
} }
export function formatItem(item: Item) { export function formatItem(item: Item | undefined) {
if (!item) return '? **MISSINGNO**';
return `${item.emoji} **${item.name}**`; return `${item.emoji} **${item.name}**`;
} }
export function formatItems(item: Item, quantity: number) { export function formatItems(item: Item | undefined, quantity: number) {
return `${quantity}x ${formatItem(item)}`; return `${quantity}x ${formatItem(item)}`;
}
export async function itemAutocomplete(interaction: AutocompleteInteraction) {
const focused = interaction.options.getFocused();
const customItems = await db<CustomItem>('customItems')
.select('emoji', 'name', 'id')
// @ts-expect-error this LITERALLY works
.whereLike(db.raw('UPPER(name)'), `%${focused.toUpperCase()}%`)
.where('guild', interaction.guildId!)
.limit(25);
const foundDefaultItems = defaultItems.filter(item => item.name.toUpperCase().includes(focused.toUpperCase()));
const items = [...foundDefaultItems, ...customItems];
await interaction.respond(
items.map(choice => ({ name: `${choice.emoji} ${choice.name}`, value: choice.id.toString() }))
);
} }