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 { 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 <t:${Math.floor((Date.now() + INVINCIBLE_TIMER) / 1000)}:R> (or if they perform an action first).`);
},
autocomplete: weaponAutocomplete,
autocomplete: weaponInventoryAutocomplete,
} 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 { 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<Either<null, string>>,
// returns Left upon success with an optional message, the reason for failure otherwise (Right)
onUse?: (value: number | undefined, item: Item, user: string) => Promise<Either<string | null, string>>,
// 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<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() : ''}`;
@ -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);
},

View File

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