item groundwork
This commit is contained in:
parent
cd0f7d5140
commit
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,257 @@
|
||||||
|
import { AutocompleteInteraction, Interaction, SlashCommandBuilder } from 'discord.js';
|
||||||
|
import { CustomItem, ItemInventory, db } from '../lib/db';
|
||||||
|
import { behaviors, defaultItems, formatItems, getItem, getMaxStack } 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 storedItem = await db<ItemInventory>('itemInventories')
|
||||||
|
.where('user', user.id)
|
||||||
|
.where('item', itemID)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
let inv;
|
||||||
|
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)}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: async (interaction: AutocompleteInteraction) => {
|
||||||
|
const focused = interaction.options.getFocused(true);
|
||||||
|
|
||||||
|
if (focused.name === 'item') {
|
||||||
|
const customItems = await db<CustomItem>('customItems')
|
||||||
|
.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() }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -44,4 +44,22 @@ export interface CounterConfiguration {
|
||||||
id: number,
|
id: number,
|
||||||
configName: string,
|
configName: string,
|
||||||
value: 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,65 @@
|
||||||
|
import { User } from 'discord.js';
|
||||||
|
import { CustomItem, 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 function getMaxStack(item: Item) {
|
||||||
|
return item.type === 'weapon' ? 1 : item.maxStack;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatItem(item: Item) {
|
||||||
|
return `${item.emoji} **${item.name}**`;
|
||||||
|
}
|
||||||
|
export function formatItems(item: Item, quantity: number) {
|
||||||
|
return `${quantity}x ${formatItem(item)}`;
|
||||||
|
}
|
Loading…
Reference in New Issue