item groundwork

This commit is contained in:
Jill 2023-11-15 03:40:57 +03:00
parent cd0f7d5140
commit 2a2cdb8dff
Signed by: oat
GPG Key ID: 33489AA58A955108
4 changed files with 374 additions and 0 deletions

View File

@ -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');
};

257
src/commands/item.ts Normal file
View File

@ -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() }))
);
}
}
};

View File

@ -44,4 +44,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
}

65
src/lib/items.ts Normal file
View File

@ -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)}`;
}