import { Client, CommandInteraction, GuildMember, EmbedBuilder, TextChannel, AutocompleteInteraction, User } from 'discord.js'; 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') .select('value') .where('id', id) .first(); if (!counter) throw 'No such counter'; return counter.value; } export async function changeCounter(id: number, delta: number) { const value = await getCounter(id); const newValue = value + delta; await db('counters') .where('id', id) .update({ 'value': newValue }); return newValue; } export async function getCounterData(id: number) { const counter = await db('counters') .select('*') .where('id', id) .first(); if (!counter) throw 'No such counter'; return counter; } export async function findCounter(key: string, guild: string) { const counter = await db('counters') .select('*') .where('key', key) .where('guild', guild) .first(); if (!counter) throw 'No such counter'; return counter; } export async function getCounterConfigRaw(counter: Counter) { const configs = await db('counterConfigurations') .select('configName', 'value') .where('id', counter.id); const config = new Map(); configs.forEach(({ configName, value }) => { config.set(configName, value); }); // just the ugly way of life config.set('emoji', counter.emoji); return config; } export async function getCounterConfig(id: number, key: string) { const config = await db('counterConfigurations') .select('value') .where('id', id) .where('configName', key) .first(); const valueStr = config?.value; let value; if (valueStr) { value = parseConfig(valueStr, counterConfigs.get(key)!.type); } else { value = counterConfigs.get(key)!.default; } return value; } export async function setCounterConfig(counter: Counter, option: string, value: string) { // just the ugly way of life if (option === 'emoji' && !counter.linkedItem) { await db('counters') .where('id', counter.id) .update({ emoji: value }); return; } const updated = await db('counterConfigurations') .update({ value: value }) .where('id', counter.id) .where('configName', option); if (updated === 0) { await db('counterConfigurations') .insert({ id: counter.id, configName: option, value: value }); } } export enum ConfigType { Bool, String, Number } export function parseConfig(str: string, type: ConfigType.Bool): boolean export function parseConfig(str: string, type: ConfigType.String): string export function parseConfig(str: string, type: ConfigType.Number): number export function parseConfig(str: string, type: ConfigType): boolean | string | number export function parseConfig(str: string, type: ConfigType) { switch(type) { case ConfigType.Bool: return str === 'true'; case ConfigType.String: return str.trim(); case ConfigType.Number: { const n = parseInt(str); if (isNaN(n)) throw 'Not a valid number'; return n; } } } export function getOptions(type: ConfigType): string[] { switch(type) { case ConfigType.Bool: return ['true', 'false']; case ConfigType.String: return []; case ConfigType.Number: return []; } } export function toStringConfig(value: boolean | string | number, type: ConfigType): string { switch(type) { case ConfigType.Bool: return value ? 'true' : 'false'; case ConfigType.String: return (value as string); case ConfigType.Number: return (value as number).toString(); } } export const counterConfigs = new Map([ ['anonymous', { type: ConfigType.Bool, default: false }], ['messageTemplate', { type: ConfigType.String, default: '**%user** has %action the counter by **%amt**.' }], ['messageTemplateIncrease', { type: ConfigType.String, default: 'null' }], ['messageTemplateDecrease', { type: ConfigType.String, 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 }], ['min', { type: ConfigType.Number, default: -Number.MIN_SAFE_INTEGER }], ['max', { type: ConfigType.Number, default: Number.MAX_SAFE_INTEGER }], // these ones are fake and are just stand-ins for values defined inside the actual counters table ['emoji', { type: ConfigType.String, default: '' }] ]); export async function updateCounter(bot: Client, counter: Counter, value: number) { const channel = await bot.channels.fetch(counter.channel) as TextChannel; const messageID = counter.message; const content = `[${counter.emoji}] x${value}`; // bit janky // yeah you don't say try { if (messageID) { const message = await channel.messages.fetch(messageID); if (!message) throw new Error(); await message.edit(content); } else { throw new Error(); } } catch(err) { const message = await channel.send(content); message.pin(); await db('counters') .where('id', counter.id) .update({ message: message.id }); } } 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; let template = await getCounterConfig(counter.id, 'messageTemplate') as string; const templateIncrease = await getCounterConfig(counter.id, 'messageTemplateIncrease') as string; if (templateIncrease !== 'null' && delta > 0) template = templateIncrease; const templateDecrease = await getCounterConfig(counter.id, 'messageTemplateDecrease') as string; 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 embed = new EmbedBuilder() //.setDescription(`**${member.toString()}** has ${delta > 0 ? 'increased' : 'decreased'} the counter by **${Math.abs(delta)}**.`) .setDescription( template .replaceAll('%user', anonymous ? 'someone' : member.toString()) .replaceAll('%action', delta > 0 ? (linked ? 'put into' : 'increased') : (linked ? 'taken from' : 'decreased')) .replaceAll('%amt', Math.abs(delta).toString()) .replaceAll('%total', value.toString()) ) .setTimestamp() .setFooter({ text: `[${counter.emoji}] x${value}` }); if (!anonymous) { embed .setAuthor({ name: member.displayName, iconURL: member.displayAvatarURL() }) .setColor(member.displayColor); } await channel.send({ embeds: [embed] }); } async function canUseCounter(user: User, counter: Counter, amount: number, isLinkedAction = false): Promise { if (amount > 0 && !(await getCounterConfig(counter.id, isLinkedAction ? 'canPut' : 'canIncrement') as boolean)) return false; if (amount > 0 && counter.allowlistProducer) { const userLink = await db('counterUserLink') .where('id', counter.id) .where('user', user.id) .where('producer', true) .first(); if (!userLink) return false; } if (amount < 0 && !(await getCounterConfig(counter.id, isLinkedAction ? 'canTake' : 'canDecrement') as boolean)) return false; if (amount < 0 && counter.allowlistConsumer) { const userLink = await db('counterUserLink') .where('id', counter.id) .where('user', user.id) .where('producer', false) .first(); if (!userLink) return false; } return true; } function changeCounterInteractionBuilder(linked: boolean) { return async (interaction: CommandInteraction, member: GuildMember, amount: number, type: string) => { try { const counter = await findCounter(type, member.guild.id); if (linked && !counter.linkedItem) return interaction.followUp('There is no such linked counter!'); const canUse = await canUseCounter(member.user, counter, amount, linked); if (!canUse) { return interaction.followUp(`You cannot **${amount > 0 ? (linked ? 'put' : 'produce') : (linked ? 'take' : 'consume')}** ${counter.emoji}.`); } const min = await getCounterConfig(counter.id, 'min') as number; const max = await getCounterConfig(counter.id, 'max') as number; if (counter.value + amount < min) { if (min === 0) { return interaction.followUp(`You cannot remove more than how much is in the counter (${counter.value}x ${counter.emoji})!`); } else { return interaction.followUp(`You cannot decrement past the minimum value (${min})!`); } } if (counter.value + amount > max) { return interaction.followUp(`You are adding more than the counter can hold (${max}x)!`); } let item; let newInv; if (linked) { const inv = await getItemQuantity(member.id, counter.linkedItem!); item = (await getItem(counter.linkedItem!))!; // change counter by -10 = increment own counter by 10 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); } await resetInvincible(member.id); 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): Autocomplete { return async (interaction: AutocompleteInteraction) => { const focusedValue = interaction.options.getFocused(); const guild = interaction.guildId; const query = db('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; return foundCounters.map(choice => ({ name: `${choice.emoji} ${choice.key}`, value: choice.key })); }; } export const counterAutocomplete = counterAutocompleteBuilder(false); export const linkedCounterAutocomplete = counterAutocompleteBuilder(true);