using and eating items, behaviors implemented

This commit is contained in:
Jill 2023-11-22 22:02:00 +03:00
parent 1a8d49ae9f
commit 4e35d47854
Signed by: oat
GPG Key ID: 33489AA58A955108
5 changed files with 150 additions and 15 deletions

View File

@ -1,5 +1,5 @@
import { GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js'; 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 { Command } from '../types/index';
import { initHealth, dealDamage, BLOOD_ITEM, BLOOD_ID, resetInvincible, INVINCIBLE_TIMER, getInvincibleMs } from '../lib/rpg/pvp'; 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 <t:${Math.floor((Date.now() + INVINCIBLE_TIMER) / 1000)}:R> (or if they perform an action first).`); 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 <t:${Math.floor((Date.now() + INVINCIBLE_TIMER) / 1000)}:R> (or if they perform an action first).`);
}, },
autocomplete: weaponAutocomplete, autocomplete: weaponInventoryAutocomplete,
} satisfies Command; } satisfies Command;

60
src/commands/eat.ts Normal file
View File

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

58
src/commands/use.ts Normal file
View File

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

View File

@ -1,5 +1,5 @@
import { giveItem, type Item, isDefaultItem } from './items'; import { giveItem, type Item, isDefaultItem, formatItems } from './items';
import { Either, Left } from '../util'; import { Either, Left, Right } from '../util';
import { ItemBehavior, db } from '../db'; import { ItemBehavior, db } from '../db';
import { BLOOD_ITEM, dealDamage } from './pvp'; import { BLOOD_ITEM, dealDamage } from './pvp';
@ -12,11 +12,11 @@ export interface Behavior {
// triggers upon use // triggers upon use
// for 'weapons', this is on attack // for 'weapons', this is on attack
// for 'consumable' and `plain`, this is on use // for 'consumable' and `plain`, this is on use
// returns Left upon success, the reason for failure otherwise (Right) // returns Left upon success with an optional message, the reason for failure otherwise (Right)
onUse?: (value: number, item: Item, user: string) => Promise<Either<null, string>>, onUse?: (value: number | undefined, item: Item, user: string) => Promise<Either<string | null, string>>,
// triggers upon `weapons` attack // triggers upon `weapons` attack
// returns the new damage value upon success (Left), the reason for failure otherwise (Right) // 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<Either<number, string>>, onAttack?: (value: number | undefined, item: Item, user: string, target: string, damage: number) => Promise<Either<number, string>>,
} }
const defaultFormat = (behavior: Behavior, value?: number) => `${behavior.name}${value ? ' ' + value.toString() : ''}`; 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`', description: 'Heals the user by `value`',
itemType: 'consumable', itemType: 'consumable',
async onUse(value, item, user) { async onUse(value, item, user) {
if (!value) return new Right('A value is required for this behavior');
await dealDamage(user, -Math.floor(value)); 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', description: 'Damages the user by `value',
itemType: 'consumable', itemType: 'consumable',
async onUse(value, item, user) { async onUse(value, item, user) {
if (!value) return new Right('A value is required for this behavior');
await dealDamage(user, Math.floor(value)); 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', itemType: 'weapon',
format: (value) => `random +${value}`, format: (value) => `random +${value}`,
async onAttack(value, item, user, target, damage) { 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)); return new Left(damage + Math.round(Math.random() * value));
}, },
}, },
@ -55,6 +58,7 @@ export const behaviors: Behavior[] = [
itemType: 'weapon', itemType: 'weapon',
format: (value) => `random -${value}`, format: (value) => `random -${value}`,
async onAttack(value, item, user, target, damage) { 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)); return new Left(damage - Math.round(Math.random() * value));
}, },
}, },
@ -64,6 +68,7 @@ export const behaviors: Behavior[] = [
itemType: 'weapon', itemType: 'weapon',
format: (value) => `lifesteal: ${value}x`, format: (value) => `lifesteal: ${value}x`,
async onAttack(value, item, user, target, damage) { 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)); giveItem(user, BLOOD_ITEM, Math.floor(damage * value));
return new Left(damage); return new Left(damage);
}, },

View File

@ -84,7 +84,8 @@ export const defaultItems: DefaultItem[] = [
emoji: '🍎', emoji: '🍎',
type: 'consumable', type: 'consumable',
maxStack: 16, maxStack: 16,
untradable: false untradable: false,
behaviors: [{ behavior: 'heal', value: 10 }],
}, },
{ {
id: -6, id: -6,
@ -93,7 +94,8 @@ export const defaultItems: DefaultItem[] = [
emoji: '🍓', emoji: '🍓',
type: 'consumable', type: 'consumable',
maxStack: 16, maxStack: 16,
untradable: false untradable: false,
behaviors: [{ behavior: 'heal', value: 4 }],
}, },
{ {
id: -7, id: -7,
@ -129,7 +131,8 @@ export const defaultItems: DefaultItem[] = [
emoji: '🪱', emoji: '🪱',
type: 'consumable', type: 'consumable',
maxStack: 128, maxStack: 128,
untradable: false untradable: false,
behaviors: [{ behavior: 'heal', value: 1 }],
}, },
{ {
id: -11, id: -11,
@ -282,7 +285,8 @@ export const defaultItems: DefaultItem[] = [
emoji: '🍱', emoji: '🍱',
type: 'consumable', type: 'consumable',
maxStack: 16, 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(' '); 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) => { return async (interaction: AutocompleteInteraction) => {
const focused = interaction.options.getFocused(); const focused = interaction.options.getFocused();
@ -391,11 +395,16 @@ function createItemAutocomplete(onlyCustom: boolean, filterType: 'plain' | 'weap
.limit(25); .limit(25);
if (filterType) itemQuery.where('type', filterType); 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; const customItems = await itemQuery;
let foundDefaultItems = defaultItems.filter(item => item.name.toUpperCase().includes(focused.toUpperCase())); let foundDefaultItems = defaultItems.filter(item => item.name.toUpperCase().includes(focused.toUpperCase()));
if (filterType) foundDefaultItems = foundDefaultItems.filter(i => i.type === filterType); 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; let items;
if (onlyCustom) { if (onlyCustom) {
@ -412,4 +421,7 @@ export const itemAutocomplete = createItemAutocomplete(false, null);
export const customItemAutocomplete = createItemAutocomplete(true, null); export const customItemAutocomplete = createItemAutocomplete(true, null);
export const plainAutocomplete = createItemAutocomplete(false, 'plain'); export const plainAutocomplete = createItemAutocomplete(false, 'plain');
export const weaponAutocomplete = createItemAutocomplete(false, 'weapon'); export const weaponAutocomplete = createItemAutocomplete(false, 'weapon');
export const consumableAutocomplete = createItemAutocomplete(false, 'consumable'); 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);