406 lines
13 KiB
TypeScript
406 lines
13 KiB
TypeScript
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<Counter>('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<Counter>('counters')
|
|
.where('id', id)
|
|
.update({
|
|
'value': newValue
|
|
});
|
|
|
|
return newValue;
|
|
}
|
|
|
|
export async function getCounterData(id: number) {
|
|
const counter = await db<Counter>('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<Counter>('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<CounterConfiguration>('counterConfigurations')
|
|
.select('configName', 'value')
|
|
.where('id', counter.id);
|
|
|
|
const config = new Map<string, string>();
|
|
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<CounterConfiguration>('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<Counter>('counters')
|
|
.where('id', counter.id)
|
|
.update({
|
|
emoji: value
|
|
});
|
|
return;
|
|
}
|
|
|
|
const updated = await db<CounterConfiguration>('counterConfigurations')
|
|
.update({
|
|
value: value
|
|
})
|
|
.where('id', counter.id)
|
|
.where('configName', option);
|
|
|
|
if (updated === 0) {
|
|
await db<CounterConfiguration>('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<Counter>('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<boolean> {
|
|
if (amount > 0 && !(await getCounterConfig(counter.id, isLinkedAction ? 'canPut' : 'canIncrement') as boolean)) return false;
|
|
if (amount > 0 && counter.allowlistProducer) {
|
|
const userLink = await db<CounterUserLink>('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>('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<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;
|
|
|
|
return foundCounters.map(choice => ({ name: `${choice.emoji} ${choice.key}`, value: choice.key }));
|
|
};
|
|
}
|
|
|
|
export const counterAutocomplete = counterAutocompleteBuilder(false);
|
|
export const linkedCounterAutocomplete = counterAutocompleteBuilder(true); |