From 4e35d478548d779a34d92cbe53a9e7e50f220d73 Mon Sep 17 00:00:00 2001 From: "Jill \"oatmealine\" Monoids" Date: Wed, 22 Nov 2023 22:02:00 +0300 Subject: [PATCH] using and eating items, behaviors implemented --- src/commands/attack.ts | 4 +-- src/commands/eat.ts | 60 ++++++++++++++++++++++++++++++++++++++++ src/commands/use.ts | 58 ++++++++++++++++++++++++++++++++++++++ src/lib/rpg/behaviors.ts | 19 ++++++++----- src/lib/rpg/items.ts | 24 ++++++++++++---- 5 files changed, 150 insertions(+), 15 deletions(-) create mode 100644 src/commands/eat.ts create mode 100644 src/commands/use.ts diff --git a/src/commands/attack.ts b/src/commands/attack.ts index 462d877..a146e7a 100644 --- a/src/commands/attack.ts +++ b/src/commands/attack.ts @@ -1,5 +1,5 @@ import { GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js'; -import { weaponAutocomplete, getItem, getItemQuantity, formatItems, formatItem } from '../lib/rpg/items'; +import { getItem, getItemQuantity, formatItems, formatItem, weaponInventoryAutocomplete } from '../lib/rpg/items'; import { Command } from '../types/index'; import { initHealth, dealDamage, BLOOD_ITEM, BLOOD_ID, resetInvincible, INVINCIBLE_TIMER, getInvincibleMs } from '../lib/rpg/pvp'; @@ -48,5 +48,5 @@ export default { await interaction.followUp(`You hit ${user} with ${formatItem(weapon)} for ${BLOOD_ITEM.emoji} **${dmg}** damage! They are now at ${formatItems(BLOOD_ITEM, newHealth.quantity)}.\nYou can attack them again (or if they perform an action first).`); }, - autocomplete: weaponAutocomplete, + autocomplete: weaponInventoryAutocomplete, } satisfies Command; \ No newline at end of file diff --git a/src/commands/eat.ts b/src/commands/eat.ts new file mode 100644 index 0000000..933d334 --- /dev/null +++ b/src/commands/eat.ts @@ -0,0 +1,60 @@ +import { GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { Command } from '../types/index'; +import { initHealth, resetInvincible } from '../lib/rpg/pvp'; +import { consumableInventoryAutocomplete, formatItem, formatItems, getItem, getItemQuantity, giveItem } from '../lib/rpg/items'; +import { getBehavior, getBehaviors } from '../lib/rpg/behaviors'; +import { Right } from '../lib/util'; + +export default { + data: new SlashCommandBuilder() + .setName('eat') + .setDescription('Eat an item from your inventory') + .addStringOption(option => + option + .setName('item') + .setAutocomplete(true) + .setDescription('The item to eat') + .setRequired(true) + ) + .setDMPermission(false), + + execute: async (interaction: CommandInteraction) => { + if (!interaction.isChatInputCommand()) return; + + const member = interaction.member! as GuildMember; + + await initHealth(member.id); + + await interaction.deferReply({ ephemeral: true }); + + const itemID = parseInt(interaction.options.getString('item', true)); + const item = await getItem(itemID); + if (!item) return await interaction.followUp('Item does not exist!'); + + const itemInv = await getItemQuantity(member.id, item.id); + if (itemInv.quantity <= 0) return await interaction.followUp(`You do not have ${formatItem(item)}!`); + + const behaviors = await getBehaviors(item); + + const messages = []; + for (const itemBehavior of behaviors) { + const behavior = getBehavior(itemBehavior.behavior); + if (!behavior) continue; + if (!behavior.onUse) continue; + const res = await behavior.onUse(itemBehavior.value, item, member.id); + if (res instanceof Right) { + await interaction.followUp(`You tried to eat ${formatItems(item, 1)}... but failed!\n${res.getValue()}`); + return; + } else { + messages.push(res.getValue()); + } + } + + await resetInvincible(member.id); + const newInv = await giveItem(member.id, item, -1); + + return await interaction.followUp(`You ate ${formatItems(item, 1)}!\n${messages.map(m => `_${m}_`).join('\n')}\nYou now have ${formatItems(item, newInv.quantity)}`); + }, + + autocomplete: consumableInventoryAutocomplete, +} satisfies Command; \ No newline at end of file diff --git a/src/commands/use.ts b/src/commands/use.ts new file mode 100644 index 0000000..85991ce --- /dev/null +++ b/src/commands/use.ts @@ -0,0 +1,58 @@ +import { GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { Command } from '../types/index'; +import { initHealth, resetInvincible } from '../lib/rpg/pvp'; +import { formatItem, getItem, getItemQuantity, plainInventoryAutocomplete } from '../lib/rpg/items'; +import { getBehavior, getBehaviors } from '../lib/rpg/behaviors'; +import { Right } from '../lib/util'; + +export default { + data: new SlashCommandBuilder() + .setName('use') + .setDescription('Use an item from your inventory') + .addStringOption(option => + option + .setName('item') + .setAutocomplete(true) + .setDescription('The item to use') + .setRequired(true) + ) + .setDMPermission(false), + + execute: async (interaction: CommandInteraction) => { + if (!interaction.isChatInputCommand()) return; + + const member = interaction.member! as GuildMember; + + await initHealth(member.id); + + await interaction.deferReply({ ephemeral: true }); + + const itemID = parseInt(interaction.options.getString('item', true)); + const item = await getItem(itemID); + if (!item) return await interaction.followUp('Item does not exist!'); + + const itemInv = await getItemQuantity(member.id, item.id); + if (itemInv.quantity <= 0) return await interaction.followUp(`You do not have ${formatItem(item)}!`); + + const behaviors = await getBehaviors(item); + + const messages = []; + for (const itemBehavior of behaviors) { + const behavior = getBehavior(itemBehavior.behavior); + if (!behavior) continue; + if (!behavior.onUse) continue; + const res = await behavior.onUse(itemBehavior.value, item, member.id); + if (res instanceof Right) { + await interaction.followUp(`You tried to use ${formatItem(item)}... but failed!\n${res.getValue()}`); + return; + } else { + messages.push(res.getValue()); + } + } + + await resetInvincible(member.id); + return await interaction.followUp(`You used ${formatItem(item)}!\n${messages.map(m => `_${m}_`).join('\n')}`); + }, + + autocomplete: plainInventoryAutocomplete, +} satisfies Command; \ No newline at end of file diff --git a/src/lib/rpg/behaviors.ts b/src/lib/rpg/behaviors.ts index fd07ced..d58a8cc 100644 --- a/src/lib/rpg/behaviors.ts +++ b/src/lib/rpg/behaviors.ts @@ -1,5 +1,5 @@ -import { giveItem, type Item, isDefaultItem } from './items'; -import { Either, Left } from '../util'; +import { giveItem, type Item, isDefaultItem, formatItems } from './items'; +import { Either, Left, Right } from '../util'; import { ItemBehavior, db } from '../db'; import { BLOOD_ITEM, dealDamage } from './pvp'; @@ -12,11 +12,11 @@ export interface Behavior { // triggers upon use // for 'weapons', this is on attack // for 'consumable' and `plain`, this is on use - // returns Left upon success, the reason for failure otherwise (Right) - onUse?: (value: number, item: Item, user: string) => Promise>, + // returns Left upon success with an optional message, the reason for failure otherwise (Right) + onUse?: (value: number | undefined, item: Item, user: string) => Promise>, // triggers upon `weapons` attack // returns the new damage value upon success (Left), the reason for failure otherwise (Right) - onAttack?: (value: number, item: Item, user: string, target: string, damage: number) => Promise>, + onAttack?: (value: number | undefined, item: Item, user: string, target: string, damage: number) => Promise>, } const defaultFormat = (behavior: Behavior, value?: number) => `${behavior.name}${value ? ' ' + value.toString() : ''}`; @@ -27,8 +27,9 @@ export const behaviors: Behavior[] = [ description: 'Heals the user by `value`', itemType: 'consumable', async onUse(value, item, user) { + if (!value) return new Right('A value is required for this behavior'); await dealDamage(user, -Math.floor(value)); - return new Left(null); + return new Left(`You were healed by ${formatItems(BLOOD_ITEM, value)}!`); }, }, { @@ -36,8 +37,9 @@ export const behaviors: Behavior[] = [ description: 'Damages the user by `value', itemType: 'consumable', async onUse(value, item, user) { + if (!value) return new Right('A value is required for this behavior'); await dealDamage(user, Math.floor(value)); - return new Left(null); + return new Left(`You were damaged by ${formatItems(BLOOD_ITEM, value)}!`); }, }, { @@ -46,6 +48,7 @@ export const behaviors: Behavior[] = [ itemType: 'weapon', format: (value) => `random +${value}`, async onAttack(value, item, user, target, damage) { + if (!value) return new Right('A value is required for this behavior'); return new Left(damage + Math.round(Math.random() * value)); }, }, @@ -55,6 +58,7 @@ export const behaviors: Behavior[] = [ itemType: 'weapon', format: (value) => `random -${value}`, async onAttack(value, item, user, target, damage) { + if (!value) return new Right('A value is required for this behavior'); return new Left(damage - Math.round(Math.random() * value)); }, }, @@ -64,6 +68,7 @@ export const behaviors: Behavior[] = [ itemType: 'weapon', format: (value) => `lifesteal: ${value}x`, async onAttack(value, item, user, target, damage) { + if (!value) return new Right('A value is required for this behavior'); giveItem(user, BLOOD_ITEM, Math.floor(damage * value)); return new Left(damage); }, diff --git a/src/lib/rpg/items.ts b/src/lib/rpg/items.ts index 56a4f56..fa0876e 100644 --- a/src/lib/rpg/items.ts +++ b/src/lib/rpg/items.ts @@ -84,7 +84,8 @@ export const defaultItems: DefaultItem[] = [ emoji: '🍎', type: 'consumable', maxStack: 16, - untradable: false + untradable: false, + behaviors: [{ behavior: 'heal', value: 10 }], }, { id: -6, @@ -93,7 +94,8 @@ export const defaultItems: DefaultItem[] = [ emoji: '🍓', type: 'consumable', maxStack: 16, - untradable: false + untradable: false, + behaviors: [{ behavior: 'heal', value: 4 }], }, { id: -7, @@ -129,7 +131,8 @@ export const defaultItems: DefaultItem[] = [ emoji: '🪱', type: 'consumable', maxStack: 128, - untradable: false + untradable: false, + behaviors: [{ behavior: 'heal', value: 1 }], }, { id: -11, @@ -282,7 +285,8 @@ export const defaultItems: DefaultItem[] = [ emoji: '🍱', type: 'consumable', maxStack: 16, - untradable: false + untradable: false, + behaviors: [{ behavior: 'heal', value: 35 }], }, ]; @@ -379,7 +383,7 @@ export function formatItemsArray(items: Items[], disableBold = false) { return items.map(i => formatItems(i.item, i.quantity, disableBold)).join(' '); } -function createItemAutocomplete(onlyCustom: boolean, filterType: 'plain' | 'weapon' | 'consumable' | null): Autocomplete { +function createItemAutocomplete(onlyCustom: boolean, filterType: 'plain' | 'weapon' | 'consumable' | null, inventory: boolean = false): Autocomplete { return async (interaction: AutocompleteInteraction) => { const focused = interaction.options.getFocused(); @@ -391,11 +395,16 @@ function createItemAutocomplete(onlyCustom: boolean, filterType: 'plain' | 'weap .limit(25); if (filterType) itemQuery.where('type', filterType); + if (inventory) itemQuery + .innerJoin('itemInventories', 'itemInventories.item', '=', 'customItems.id') + .where('itemInventories.user', '=', interaction.member!.user.id) + .where('itemInventories.quantity', '>', '0'); const customItems = await itemQuery; let foundDefaultItems = defaultItems.filter(item => item.name.toUpperCase().includes(focused.toUpperCase())); if (filterType) foundDefaultItems = foundDefaultItems.filter(i => i.type === filterType); + if (inventory) foundDefaultItems = (await Promise.all(foundDefaultItems.map(async i => ({...i, inv: await getItemQuantity(interaction.member!.user.id, i.id)})))).filter(i => i.inv.quantity > 0); let items; if (onlyCustom) { @@ -412,4 +421,7 @@ export const itemAutocomplete = createItemAutocomplete(false, null); export const customItemAutocomplete = createItemAutocomplete(true, null); export const plainAutocomplete = createItemAutocomplete(false, 'plain'); export const weaponAutocomplete = createItemAutocomplete(false, 'weapon'); -export const consumableAutocomplete = createItemAutocomplete(false, 'consumable'); \ No newline at end of file +export const consumableAutocomplete = createItemAutocomplete(false, 'consumable'); +export const plainInventoryAutocomplete = createItemAutocomplete(false, 'plain', true); +export const weaponInventoryAutocomplete = createItemAutocomplete(false, 'weapon', true); +export const consumableInventoryAutocomplete = createItemAutocomplete(false, 'consumable', true); \ No newline at end of file