diff --git a/src/commands/attack.ts b/src/commands/attack.ts index a146e7a..4089443 100644 --- a/src/commands/attack.ts +++ b/src/commands/attack.ts @@ -2,6 +2,8 @@ import { GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js 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'; +import { getBehavior, getBehaviors } from '../lib/rpg/behaviors'; +import { Right } from '../lib/util'; export default { data: new SlashCommandBuilder() @@ -28,7 +30,7 @@ export default { const member = interaction.member! as GuildMember; await initHealth(member.id); const weaponID = parseInt(interaction.options.getString('weapon', true)); - const user = interaction.options.getUser('user', true); + const target = interaction.options.getUser('user', true); await interaction.deferReply({ephemeral: true}); @@ -37,15 +39,40 @@ export default { if (weapon.type !== 'weapon') return interaction.followUp('That is not a weapon!'); const inv = await getItemQuantity(member.id, weapon.id); if (inv.quantity === 0) return interaction.followUp('You do not have this weapon!'); - const invinTimer = await getInvincibleMs(user.id); + const invinTimer = await getInvincibleMs(target.id); if (invinTimer > 0) return interaction.followUp(`You can only attack this user (or if they perform an action first)!`); - const dmg = weapon.maxStack; - await dealDamage(user.id, dmg); - const newHealth = await getItemQuantity(user.id, BLOOD_ID); + let dmg = weapon.maxStack; + const messages = []; - if (user.id !== member.id) await resetInvincible(member.id); - 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).`); + const behaviors = await getBehaviors(weapon); + for (const itemBehavior of behaviors) { + const behavior = getBehavior(itemBehavior.behavior); + if (!behavior) continue; + if (!behavior.onAttack) continue; + const res = await behavior.onAttack({ + value: itemBehavior.value, + damage: dmg, + item: weapon, + user: member.id, + target: target.id, + }); + if (res instanceof Right) { + await interaction.followUp(`You tried to attack with ${formatItem(weapon)}... but failed!\n${res.getValue()}`); + return; + } else { + const { message, damage } = res.getValue(); + if (message) messages.push(message); + if (damage) dmg = damage; + } + } + + await dealDamage(target.id, dmg); + const newHealth = await getItemQuantity(target.id, BLOOD_ID); + + if (target.id !== member.id) await resetInvincible(member.id); + + await interaction.followUp(`You hit ${target} with ${formatItem(weapon)} for ${BLOOD_ITEM.emoji} **${dmg}** damage!\n${messages.map(m => `_${m}_\n`).join('')}They are now at ${formatItems(BLOOD_ITEM, newHealth.quantity)}.\nYou can attack them again (or if they perform an action first).`); }, autocomplete: weaponInventoryAutocomplete, diff --git a/src/commands/eat.ts b/src/commands/eat.ts index 933d334..5e389ee 100644 --- a/src/commands/eat.ts +++ b/src/commands/eat.ts @@ -41,7 +41,11 @@ export default { const behavior = getBehavior(itemBehavior.behavior); if (!behavior) continue; if (!behavior.onUse) continue; - const res = await behavior.onUse(itemBehavior.value, item, member.id); + const res = await behavior.onUse({ + value: itemBehavior.value, + item, + user: member.id + }); if (res instanceof Right) { await interaction.followUp(`You tried to eat ${formatItems(item, 1)}... but failed!\n${res.getValue()}`); return; diff --git a/src/commands/item.ts b/src/commands/item.ts index 0e291fd..9f97aac 100644 --- a/src/commands/item.ts +++ b/src/commands/item.ts @@ -332,11 +332,11 @@ export default { if (itemID) { const item = await getItem(parseInt(itemID)); if (item) { - foundBehaviors = foundBehaviors.filter(b => b.itemType === item.type); + foundBehaviors = foundBehaviors.filter(b => b.type === item.type); } } - return foundBehaviors.map(b => ({name: `${b.itemType}:${b.name} - ${b.description}`, value: b.name})); + return foundBehaviors.map(b => ({name: `${b.type}:${b.name} - ${b.description}`, value: b.name})); }, removebehavior: async (interaction: AutocompleteInteraction) => { const focused = interaction.options.getFocused(); @@ -351,7 +351,7 @@ export default { .filter(b => b.behavior.name.toLowerCase().includes(focused.toLowerCase())); return foundBehaviors.map(b => ({ - name: `${b.behavior.itemType}:${formatBehavior(b.behavior, b.value)} - ${b.behavior.description}`, + name: `${b.behavior.type}:${formatBehavior(b.behavior, b.value)} - ${b.behavior.description}`, value: b.behavior.name })); }, diff --git a/src/commands/use.ts b/src/commands/use.ts index 85991ce..25dc2d1 100644 --- a/src/commands/use.ts +++ b/src/commands/use.ts @@ -41,7 +41,11 @@ export default { const behavior = getBehavior(itemBehavior.behavior); if (!behavior) continue; if (!behavior.onUse) continue; - const res = await behavior.onUse(itemBehavior.value, item, member.id); + const res = await behavior.onUse({ + value: itemBehavior.value, + item, + user: member.id + }); if (res instanceof Right) { await interaction.followUp(`You tried to use ${formatItem(item)}... but failed!\n${res.getValue()}`); return; diff --git a/src/lib/rpg/behaviors.ts b/src/lib/rpg/behaviors.ts index d58a8cc..fd6dc7b 100644 --- a/src/lib/rpg/behaviors.ts +++ b/src/lib/rpg/behaviors.ts @@ -3,20 +3,32 @@ import { Either, Left, Right } from '../util'; import { ItemBehavior, db } from '../db'; import { BLOOD_ITEM, dealDamage } from './pvp'; +interface BehaviorContext { + value: number | undefined, +} +type ItemContext = BehaviorContext & { + item: Item, + user: string, +} +type AttackContext = ItemContext & { + target: string, + damage: number, +} + export interface Behavior { name: string, description: string, - itemType: 'plain' | 'weapon' | 'consumable', + type: 'plain' | 'weapon' | 'consumable', // make it look fancy format?: (value?: number) => string, // triggers upon use // for 'weapons', this is on attack // for 'consumable' and `plain`, this is on use // returns Left upon success with an optional message, the reason for failure otherwise (Right) - onUse?: (value: number | undefined, item: Item, user: string) => Promise>, + onUse?: (ctx: ItemContext) => Promise>, // triggers upon `weapons` attack - // returns the new damage value upon success (Left), the reason for failure otherwise (Right) - onAttack?: (value: number | undefined, item: Item, user: string, target: string, damage: number) => Promise>, + // returns the new damage value if applicable upon success and an optional message (Left), the reason for failure otherwise (Right) + onAttack?: (ctx: AttackContext) => Promise>, } const defaultFormat = (behavior: Behavior, value?: number) => `${behavior.name}${value ? ' ' + value.toString() : ''}`; @@ -25,52 +37,55 @@ export const behaviors: Behavior[] = [ { name: 'heal', 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(`You were healed by ${formatItems(BLOOD_ITEM, value)}!`); + type: 'consumable', + async onUse(ctx) { + if (!ctx.value) return new Right('A value is required for this behavior'); + await dealDamage(ctx.user, -Math.floor(ctx.value)); + return new Left(`You were healed by ${formatItems(BLOOD_ITEM, ctx.value)}!`); }, }, { name: 'damage', 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(`You were damaged by ${formatItems(BLOOD_ITEM, value)}!`); + type: 'consumable', + async onUse(ctx) { + if (!ctx.value) return new Right('A value is required for this behavior'); + await dealDamage(ctx.user, Math.floor(ctx.value)); + return new Left(`You were damaged by ${formatItems(BLOOD_ITEM, ctx.value)}!`); }, }, { name: 'random_up', description: 'Randomizes the attack value up by a maximum of `value`', - itemType: 'weapon', + type: '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)); + async onAttack(ctx) { + if (!ctx.value) return new Right('A value is required for this behavior'); + return new Left({ damage: ctx.damage + Math.round(Math.random() * ctx.value) }); }, }, { name: 'random_down', description: 'Randomizes the attack value down by a maximum of `value`', - itemType: 'weapon', + type: '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)); + async onAttack(ctx) { + if (!ctx.value) return new Right('A value is required for this behavior'); + return new Left({ damage: ctx.damage - Math.round(Math.random() * ctx.value) }); }, }, { name: 'lifesteal', description: 'Gain blood by stabbing your foes, scaled by `value`x', - 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); + type: 'weapon', + format: (value) => `lifesteal ${value}x`, + async onAttack(ctx) { + if (!ctx.value) return new Right('A value is required for this behavior'); + const amt = Math.floor(ctx.damage * ctx.value); + giveItem(ctx.user, BLOOD_ITEM, amt); + return new Left({ + message: `You gained ${formatItems(BLOOD_ITEM, amt)} from your target!` + }); }, } ]; diff --git a/src/lib/rpg/pvp.ts b/src/lib/rpg/pvp.ts index 7bc89d5..62bdf3c 100644 --- a/src/lib/rpg/pvp.ts +++ b/src/lib/rpg/pvp.ts @@ -5,8 +5,8 @@ import { Client } from 'discord.js'; export const BLOOD_ID = -DefaultItems.BLOOD; export const BLOOD_ITEM = getDefaultItem(BLOOD_ID); export const MAX_HEALTH = BLOOD_ITEM.maxStack; -const BLOOD_GAIN_PER_HOUR = 5; -export const INVINCIBLE_TIMER = 1_000 * 60 * 60; +const BLOOD_GAIN_PER_HOUR = 2; +export const INVINCIBLE_TIMER = 1_000 * 60 * 30; export async function initHealth(user: string) { const isInitialized = await db('initHealth')