Compare commits
5 Commits
cd0f7d5140
...
2bb3512316
Author | SHA1 | Date |
---|---|---|
Jill | 2bb3512316 | |
Jill | 233a663d0c | |
Jill | a0c9b12da0 | |
Jill | 4351e6dfad | |
Jill | 2a2cdb8dff |
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function(knex) {
|
||||
return knex.schema
|
||||
.createTable('customItems', table => {
|
||||
table.increments('id');
|
||||
table.string('guild').notNullable();
|
||||
table.string('name').notNullable();
|
||||
table.text('description');
|
||||
table.string('emoji').notNullable();
|
||||
table.enum('type', ['plain', 'weapon', 'consumable']).notNullable();
|
||||
table.integer('maxStack').notNullable(); // or damage for weapons
|
||||
table.string('behavior');
|
||||
table.boolean('untradable').defaultTo(false);
|
||||
table.float('behaviorValue');
|
||||
})
|
||||
.createTable('itemInventories', table => {
|
||||
table.string('user').notNullable();
|
||||
table.integer('item').notNullable();
|
||||
table.integer('quantity').defaultTo(1);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function(knex) {
|
||||
return knex.schema
|
||||
.dropTable('customItems')
|
||||
.dropTable('itemInventories');
|
||||
};
|
|
@ -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');
|
||||
});
|
||||
};
|
|
@ -2,6 +2,7 @@ import { AutocompleteInteraction, Interaction, SlashCommandBuilder } from 'disco
|
|||
import { Counter, CounterUserLink, db } from '../lib/db';
|
||||
import { counterAutocomplete, counterConfigs, findCounter, getCounterConfigRaw, getOptions, parseConfig, setCounterConfig, toStringConfig, updateCounter } from '../lib/counter';
|
||||
import { outdent } from 'outdent';
|
||||
import { formatItem, formatItems, getItem, itemAutocomplete } from '../lib/items';
|
||||
|
||||
function extendOption(t: string) {
|
||||
return {name: t, value: t};
|
||||
|
@ -9,7 +10,7 @@ function extendOption(t: string) {
|
|||
|
||||
const help = new Map([
|
||||
['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:
|
||||
|
||||
> **%user** has %action the counter by **%amt**.
|
||||
|
@ -143,7 +144,7 @@ module.exports = {
|
|||
.addStringOption(option =>
|
||||
option
|
||||
.setName('key')
|
||||
.setDescription('The codename. Best to leave descriptive for later; used in searching for counters')
|
||||
.setDescription('Give your counter a simple name')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
|
@ -215,6 +216,25 @@ module.exports = {
|
|||
.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')
|
||||
.setDMPermission(false),
|
||||
|
||||
|
@ -233,10 +253,7 @@ module.exports = {
|
|||
try {
|
||||
counter = await findCounter(type, interaction.guildId!);
|
||||
} catch(err) {
|
||||
await interaction.followUp({
|
||||
content: 'No such counter!'
|
||||
});
|
||||
return;
|
||||
return interaction.followUp('No such counter!');
|
||||
}
|
||||
|
||||
if (subcommand === 'add') {
|
||||
|
@ -276,12 +293,7 @@ module.exports = {
|
|||
.where('producer', userType === 'producer')
|
||||
.first();
|
||||
|
||||
if (!link) {
|
||||
await interaction.followUp({
|
||||
content: `<@${user.id}> is not in the ${counter.emoji} **${userType}** allowlist!`
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!link) return interaction.followUp(`<@${user.id}> is not in the ${counter.emoji} **${userType}** allowlist!`);
|
||||
|
||||
await interaction.followUp({
|
||||
content: `<@${user.id}> has been removed from the ${counter.emoji} **${userType}** allowlist.`
|
||||
|
@ -350,15 +362,14 @@ module.exports = {
|
|||
try {
|
||||
counter = await findCounter(type, interaction.guildId!);
|
||||
} catch(err) {
|
||||
await interaction.followUp({
|
||||
content: 'No such counter!'
|
||||
});
|
||||
return;
|
||||
return interaction.followUp('No such counter!');
|
||||
}
|
||||
|
||||
const config = await getCounterConfigRaw(counter);
|
||||
const key = interaction.options.getString('key', 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);
|
||||
if (!defaultConfig) return interaction.followUp(`No config named \`${key}\` exists!`);
|
||||
|
@ -366,7 +377,7 @@ module.exports = {
|
|||
const parsedValue = parseConfig(value, 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)}\`)`);
|
||||
} else if (subcommand === 'delete') {
|
||||
|
@ -376,10 +387,7 @@ module.exports = {
|
|||
try {
|
||||
counter = await findCounter(type, interaction.guildId!);
|
||||
} catch(err) {
|
||||
await interaction.followUp({
|
||||
content: 'No such counter!'
|
||||
});
|
||||
return;
|
||||
return interaction.followUp('No such counter!');
|
||||
}
|
||||
|
||||
await db<Counter>('counters')
|
||||
|
@ -391,15 +399,44 @@ module.exports = {
|
|||
.delete();
|
||||
|
||||
await interaction.followUp({
|
||||
content: `The ${counter.emoji} counter has been removed. 😭`
|
||||
content: `The ${counter.emoji} ${counter.key} counter has been removed. 😭`
|
||||
});
|
||||
} else if (subcommand === 'list') {
|
||||
const counters = await db<Counter>('counters')
|
||||
.where('guild', interaction.guildId!);
|
||||
|
||||
await interaction.followUp(counters.map(c => `${c.emoji} **${c.value}** <#${c.channel}>`).join('\n'));
|
||||
await interaction.followUp(counters.map(c => `${c.emoji} ${c.key}: **${c.value}** <#${c.channel}>`).join('\n'));
|
||||
} else if (subcommand === 'help') {
|
||||
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!');
|
||||
if (item.untradable) return interaction.followUp('This item is untradable!');
|
||||
|
||||
await db<Counter>('counters')
|
||||
.where('id', counter.id)
|
||||
.update({
|
||||
'linkedItem': item.id,
|
||||
'emoji': item.emoji,
|
||||
'key': item.name,
|
||||
'value': 0
|
||||
});
|
||||
|
||||
await setCounterConfig(counter, 'canIncrement', 'false');
|
||||
await setCounterConfig(counter, 'canDecrement', 'false');
|
||||
await setCounterConfig(counter, 'min', '0');
|
||||
|
||||
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 \`min\` has been set to **0**, and you are recommended to keep these values as such if you want to maintain balance in the universe.`);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -409,6 +446,8 @@ module.exports = {
|
|||
|
||||
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!);
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import { GuildMember, Interaction, SlashCommandBuilder } from 'discord.js';
|
||||
import { ItemInventory, db } from '../lib/db';
|
||||
import { formatItems, getItem } from '../lib/items';
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('inventory')
|
||||
.setDescription('Check your inventory')
|
||||
.setDMPermission(false),
|
||||
|
||||
execute: async (interaction: Interaction, member: GuildMember) => {
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
|
||||
await interaction.deferReply({ephemeral: true});
|
||||
|
||||
const itemsList = await db<ItemInventory>('itemInventories')
|
||||
.select('item', 'quantity')
|
||||
.where('user', member.user.id);
|
||||
|
||||
// kind of stupid kind of awful
|
||||
const items = (await Promise.all(itemsList.map(async i => ({item: await getItem(i.item), quantity: i.quantity})))).filter(i => i.item);
|
||||
|
||||
await interaction.followUp(
|
||||
`Your inventory:\n${items.length === 0 ? '_Your inventory is empty!_' : items.map(i => `- ${formatItems(i.item!, i.quantity)}`).join('\n')}`
|
||||
);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,221 @@
|
|||
import { AutocompleteInteraction, Interaction, SlashCommandBuilder } from 'discord.js';
|
||||
import { CustomItem, ItemInventory, db } from '../lib/db';
|
||||
import { behaviors, formatItems, getItem, getMaxStack, giveItem, itemAutocomplete } from '../lib/items';
|
||||
|
||||
//function extendOption(t: string) {
|
||||
// return {name: t, value: t};
|
||||
//}
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('item')
|
||||
.setDescription('[ADMIN] Create, edit and otherwise deal with custom items')
|
||||
.addSubcommandGroup(grp =>
|
||||
grp
|
||||
.setName('add')
|
||||
.setDescription('[ADMIN] Create an item')
|
||||
.addSubcommand(cmd =>
|
||||
cmd
|
||||
.setName('plain')
|
||||
.setDescription('A normal, functionless item')
|
||||
.addStringOption(opt =>
|
||||
opt
|
||||
.setName('name')
|
||||
.setDescription('The item name')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(opt =>
|
||||
opt
|
||||
.setName('emoji')
|
||||
.setDescription('An emoji or symbol that could represent this item')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(opt =>
|
||||
opt
|
||||
.setName('description')
|
||||
.setDescription('A short description')
|
||||
)
|
||||
.addIntegerOption(opt =>
|
||||
opt
|
||||
.setName('maxstack')
|
||||
.setDescription('Maximum amount of this item you\'re able to hold at once')
|
||||
)
|
||||
.addStringOption(opt =>
|
||||
opt
|
||||
.setName('behavior')
|
||||
.setDescription('Special behavior type')
|
||||
.setChoices(...behaviors.filter(b => b.itemType === 'plain').map(b => ({name: `${b.name} - ${b.description}`, value: b.name})))
|
||||
)
|
||||
.addBooleanOption(opt =>
|
||||
opt
|
||||
.setName('untradable')
|
||||
.setDescription('Can you give this item to other people?')
|
||||
)
|
||||
.addNumberOption(opt =>
|
||||
opt
|
||||
.setName('behaviorvalue')
|
||||
.setDescription('A value to use for the behavior type; not always applicable')
|
||||
)
|
||||
)
|
||||
.addSubcommand(cmd =>
|
||||
cmd
|
||||
.setName('weapon')
|
||||
.setDescription('A weapon that you can attack things with')
|
||||
.addStringOption(opt =>
|
||||
opt
|
||||
.setName('name')
|
||||
.setDescription('The item name')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(opt =>
|
||||
opt
|
||||
.setName('emoji')
|
||||
.setDescription('An emoji or symbol that could represent this item')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addIntegerOption(opt =>
|
||||
opt
|
||||
.setName('damage')
|
||||
.setDescription('How much base damage this weapon is intended to deal')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(opt =>
|
||||
opt
|
||||
.setName('description')
|
||||
.setDescription('A short description')
|
||||
)
|
||||
.addStringOption(opt =>
|
||||
opt
|
||||
.setName('behavior')
|
||||
.setDescription('Special behavior type')
|
||||
.setChoices(...behaviors.filter(b => b.itemType === 'weapon').map(b => ({name: `${b.name} - ${b.description}`, value: b.name})))
|
||||
)
|
||||
.addBooleanOption(opt =>
|
||||
opt
|
||||
.setName('untradable')
|
||||
.setDescription('Can you give this item to other people?')
|
||||
)
|
||||
.addNumberOption(opt =>
|
||||
opt
|
||||
.setName('behaviorvalue')
|
||||
.setDescription('A value to use for the behavior type; not always applicable')
|
||||
)
|
||||
)
|
||||
.addSubcommand(cmd =>
|
||||
cmd
|
||||
.setName('consumable')
|
||||
.setDescription('Consumable item, usable once and never again')
|
||||
.addStringOption(opt =>
|
||||
opt
|
||||
.setName('name')
|
||||
.setDescription('The item name')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(opt =>
|
||||
opt
|
||||
.setName('emoji')
|
||||
.setDescription('An emoji or symbol that could represent this item')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(opt =>
|
||||
opt
|
||||
.setName('description')
|
||||
.setDescription('A short description')
|
||||
)
|
||||
.addIntegerOption(opt =>
|
||||
opt
|
||||
.setName('maxstack')
|
||||
.setDescription('Maximum amount of this item you\'re able to hold at once')
|
||||
)
|
||||
.addStringOption(opt =>
|
||||
opt
|
||||
.setName('behavior')
|
||||
.setDescription('Special behavior type')
|
||||
.setChoices(...behaviors.filter(b => b.itemType === 'consumable').map(b => ({name: `${b.name} - ${b.description}`, value: b.name})))
|
||||
)
|
||||
.addBooleanOption(opt =>
|
||||
opt
|
||||
.setName('untradable')
|
||||
.setDescription('Can you give this item to other people?')
|
||||
)
|
||||
.addNumberOption(opt =>
|
||||
opt
|
||||
.setName('behaviorvalue')
|
||||
.setDescription('A value to use for the behavior type; not always applicable')
|
||||
)
|
||||
)
|
||||
)
|
||||
.addSubcommand(cmd =>
|
||||
cmd
|
||||
.setName('give')
|
||||
.setDescription('[ADMIN] Give a user an item')
|
||||
.addUserOption(opt =>
|
||||
opt
|
||||
.setName('who')
|
||||
.setDescription('The user')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(opt =>
|
||||
opt
|
||||
.setName('item')
|
||||
.setDescription('The item')
|
||||
.setAutocomplete(true)
|
||||
.setRequired(true)
|
||||
)
|
||||
.addIntegerOption(opt =>
|
||||
opt
|
||||
.setName('quantity')
|
||||
.setDescription('Amount of items to give')
|
||||
)
|
||||
)
|
||||
.setDefaultMemberPermissions('0')
|
||||
.setDMPermission(false),
|
||||
|
||||
execute: async (interaction: Interaction) => {
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
|
||||
await interaction.deferReply({ephemeral: true});
|
||||
|
||||
const subcommand = interaction.options.getSubcommand(true);
|
||||
const group = interaction.options.getSubcommandGroup();
|
||||
|
||||
if (group === 'add') {
|
||||
const item = await db<CustomItem>('customItems')
|
||||
.insert({
|
||||
'guild': interaction.guildId!,
|
||||
'name': interaction.options.getString('name', true).trim(),
|
||||
'description': interaction.options.getString('description') || undefined,
|
||||
'emoji': interaction.options.getString('emoji', true).trim(),
|
||||
'type': subcommand as 'plain' | 'weapon' | 'consumable', // kind of wild that ts makes you do this
|
||||
'maxStack': (interaction.options.getInteger('maxstack') || interaction.options.getInteger('damage')) || (subcommand === 'weapon' ? 1 : 64),
|
||||
'behavior': interaction.options.getString('behavior') || undefined,
|
||||
'untradable': interaction.options.getBoolean('untradable') || false,
|
||||
'behaviorValue': interaction.options.getNumber('behaviorValue') || undefined,
|
||||
})
|
||||
.returning('*');
|
||||
|
||||
await interaction.followUp(`${JSON.stringify(item[0])}`);
|
||||
} else {
|
||||
if (subcommand === 'give') {
|
||||
const user = interaction.options.getUser('who', true);
|
||||
const itemID = parseInt(interaction.options.getString('item', true));
|
||||
const quantity = interaction.options.getInteger('quantity') || 1;
|
||||
|
||||
const item = await getItem(itemID);
|
||||
if (!item) return interaction.followUp('No such item exists!');
|
||||
|
||||
const inv = await giveItem(user.id, item, quantity);
|
||||
|
||||
await interaction.followUp(`${user.toString()} now has ${formatItems(item, inv.quantity)}.`);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
autocomplete: async (interaction: AutocompleteInteraction) => {
|
||||
const focused = interaction.options.getFocused(true);
|
||||
|
||||
if (focused.name === 'item') {
|
||||
return itemAutocomplete(interaction);
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
};
|
|
@ -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 { Counter, CounterConfiguration, CounterUserLink, db } from './db';
|
||||
import { formatItems, getItem, getItemQuantity, getMaxStack, giveItem } from './items';
|
||||
|
||||
export async function getCounter(id: number) {
|
||||
const counter = await db<Counter>('counters')
|
||||
|
@ -83,11 +84,11 @@ export async function getCounterConfig(id: number, key: string) {
|
|||
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
|
||||
if (option === 'emoji') {
|
||||
if (option === 'emoji' && !counter.linkedItem) {
|
||||
await db<Counter>('counters')
|
||||
.where('id', id)
|
||||
.where('id', counter.id)
|
||||
.update({
|
||||
'emoji': value
|
||||
});
|
||||
|
@ -98,13 +99,13 @@ export async function setCounterConfig(id: number, option: string, value: string
|
|||
.update({
|
||||
value: value
|
||||
})
|
||||
.where('id', id)
|
||||
.where('id', counter.id)
|
||||
.where('configName', option);
|
||||
|
||||
if (updated === 0) {
|
||||
await db<CounterConfiguration>('counterConfigurations')
|
||||
.insert({
|
||||
'id': id,
|
||||
'id': counter.id,
|
||||
'configName': option,
|
||||
'value': value
|
||||
});
|
||||
|
@ -174,6 +175,38 @@ export const counterConfigs = new Map([
|
|||
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', {
|
||||
|
@ -210,7 +243,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;
|
||||
|
||||
let template = await getCounterConfig(counter.id, 'messageTemplate') as string;
|
||||
|
@ -218,6 +251,10 @@ export async function announceCounterUpdate(bot: Client, member: GuildMember, de
|
|||
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;
|
||||
|
||||
|
@ -226,7 +263,7 @@ export async function announceCounterUpdate(bot: Client, member: GuildMember, de
|
|||
.setDescription(
|
||||
template
|
||||
.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('%total', value.toString())
|
||||
)
|
||||
|
@ -249,66 +286,120 @@ export async function announceCounterUpdate(bot: Client, member: GuildMember, de
|
|||
});
|
||||
}
|
||||
|
||||
export async function changeCounterInteraction(interaction: CommandInteraction, member: GuildMember, amount: number, type: string) {
|
||||
try {
|
||||
const counter = await findCounter(type, member.guild.id);
|
||||
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();
|
||||
|
||||
let canUse = true;
|
||||
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 (!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;
|
||||
}
|
||||
|
||||
export async function counterAutocomplete(interaction: AutocompleteInteraction) {
|
||||
const focusedValue = interaction.options.getFocused();
|
||||
const guild = interaction.guildId;
|
||||
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 query = db<Counter>('counters')
|
||||
.select('emoji', 'key')
|
||||
.whereLike('key', `%${focusedValue.toLowerCase()}%`)
|
||||
.limit(25);
|
||||
const canUse = await canUseCounter(member.user, counter, amount, linked);
|
||||
|
||||
if (guild) {
|
||||
query.where('guild', guild);
|
||||
}
|
||||
if (!canUse) {
|
||||
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(
|
||||
foundCounters.map(choice => ({ name: choice.emoji, value: choice.key }))
|
||||
);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
|
||||
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)!`);
|
||||
}
|
||||
|
||||
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} ${choice.key}`, value: choice.key }))
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const counterAutocomplete = counterAutocompleteBuilder(false);
|
||||
export const linkedCounterAutocomplete = counterAutocompleteBuilder(true);
|
|
@ -33,7 +33,8 @@ export interface Counter {
|
|||
guild: string,
|
||||
message?: string,
|
||||
allowlistConsumer: boolean,
|
||||
allowlistProducer: boolean
|
||||
allowlistProducer: boolean,
|
||||
linkedItem?: number
|
||||
}
|
||||
export interface CounterUserLink {
|
||||
id: number,
|
||||
|
@ -44,4 +45,22 @@ export interface CounterConfiguration {
|
|||
id: number,
|
||||
configName: string,
|
||||
value: string
|
||||
}
|
||||
export interface CustomItem {
|
||||
id: number,
|
||||
guild: string,
|
||||
name: string,
|
||||
description?: string,
|
||||
emoji: string,
|
||||
type: 'plain' | 'weapon' | 'consumable',
|
||||
// also damage for weapons; weapons are always unstackable (cus i said so)
|
||||
maxStack: number,
|
||||
behavior?: string,
|
||||
untradable: boolean,
|
||||
behaviorValue?: number
|
||||
}
|
||||
export interface ItemInventory {
|
||||
user: string,
|
||||
item: number,
|
||||
quantity: number
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
import { AutocompleteInteraction, User } from 'discord.js';
|
||||
import { CustomItem, ItemInventory, db } from './db';
|
||||
|
||||
type DefaultItem = Omit<CustomItem, 'guild'>; // uses negative IDs
|
||||
type Item = DefaultItem | CustomItem;
|
||||
|
||||
interface Behavior {
|
||||
name: string,
|
||||
description: string,
|
||||
itemType: 'plain' | 'weapon' | 'consumable',
|
||||
// triggers upon use
|
||||
// for 'weapons', this is on hit
|
||||
// for 'consumable', this is on use
|
||||
// for 'plain', ...??
|
||||
// returns `true` upon success, `false` otherwise
|
||||
action?: (item: Item, user: User) => Promise<boolean>
|
||||
}
|
||||
|
||||
export const defaultItems: DefaultItem[] = [
|
||||
{
|
||||
'id': -1,
|
||||
'name': 'Coin',
|
||||
'emoji': '🪙',
|
||||
'type': 'plain',
|
||||
'maxStack': 9999,
|
||||
'untradable': false
|
||||
}
|
||||
];
|
||||
|
||||
export const behaviors: Behavior[] = [
|
||||
{
|
||||
'name': 'heal',
|
||||
'description': 'Heals the user by `behaviorValue`',
|
||||
'itemType': 'consumable',
|
||||
'action': async (item: Item, user: User) => {
|
||||
// todo
|
||||
return false;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export async function getCustomItem(id: number) {
|
||||
return await db<CustomItem>('customItems')
|
||||
.where('id', id)
|
||||
.first();
|
||||
}
|
||||
|
||||
export async function getItem(id: number): Promise<Item | undefined> {
|
||||
if (id >= 0) {
|
||||
return await getCustomItem(id);
|
||||
} else {
|
||||
return defaultItems.find(item => item.id === id);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
return item.type === 'weapon' ? 1 : item.maxStack;
|
||||
}
|
||||
|
||||
export function formatItem(item: Item | undefined) {
|
||||
if (!item) return '? **MISSINGNO**';
|
||||
return `${item.emoji} **${item.name}**`;
|
||||
}
|
||||
export function formatItems(item: Item | undefined, quantity: number) {
|
||||
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() }))
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue