jillo-bot/src/lib/rpg/counter.ts

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);