diff --git a/src/commands/attack.ts b/src/commands/attack.ts new file mode 100644 index 0000000..b429ea4 --- /dev/null +++ b/src/commands/attack.ts @@ -0,0 +1,47 @@ +import { GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { weaponAutocomplete, getItem, getItemQuantity, formatItems } from '../lib/rpg/items'; +import { Command } from '../types/index'; +import { initHealth, dealDamage, BLOOD_ITEM, BLOOD_ID } from '../lib/rpg/pvp'; + +export default { + data: new SlashCommandBuilder() + .setName('attack') + .setDescription('Attack someone using a weapon you have') + .addStringOption(option => + option + .setName('weapon') + .setAutocomplete(true) + .setDescription('The weapon to use') + .setRequired(true) + ) + .addUserOption(option => + option + .setName('user') + .setRequired(true) + .setDescription('Who to attack with the weapon') + ) + .setDMPermission(false), + + execute: async (interaction: CommandInteraction) => { + if (!interaction.isChatInputCommand()) return; + + 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); + + await interaction.deferReply({ephemeral: true}); + + const weapon = await getItem(weaponID); + if (!weapon) return interaction.followUp('No such item exists!'); + if (weapon.type !== 'weapon') return interaction.followUp('That is not a weapon!'); + + const dmg = weapon.maxStack; + await dealDamage(user.id, dmg); + const newHealth = await getItemQuantity(user.id, BLOOD_ID); + + await interaction.followUp(`You hit ${user} for ${BLOOD_ITEM.emoji} **${dmg}** damage! They are now at ${formatItems(BLOOD_ITEM, newHealth.quantity)}.`); + }, + + autocomplete: weaponAutocomplete, +} satisfies Command; \ No newline at end of file diff --git a/src/commands/craft.ts b/src/commands/craft.ts index 5577302..ae7b910 100644 --- a/src/commands/craft.ts +++ b/src/commands/craft.ts @@ -4,6 +4,7 @@ import { getStation, canUseStation, craftingStations, verb, CraftingStation } fr import { formatItem, getItemQuantity, formatItems, getMaxStack, giveItem, formatItemsArray } from '../lib/rpg/items'; import { getRecipe, defaultRecipes, formatRecipe, resolveCustomRecipe } from '../lib/rpg/recipes'; import { Command } from '../types/index'; +import { initHealth } from '../lib/rpg/pvp'; export default { data: new SlashCommandBuilder() @@ -30,6 +31,8 @@ export default { const member = interaction.member! as GuildMember; + await initHealth(member.id); + const recipeID = parseInt(interaction.options.getString('recipe', true)); await interaction.deferReply({ephemeral: true}); diff --git a/src/commands/emotedump.ts b/src/commands/emotedump.ts index 7910e1a..11c4f8f 100644 --- a/src/commands/emotedump.ts +++ b/src/commands/emotedump.ts @@ -1,4 +1,4 @@ -import { GuildMember, EmbedBuilder, SlashCommandBuilder, CommandInteraction } from 'discord.js'; +import { EmbedBuilder, SlashCommandBuilder, CommandInteraction } from 'discord.js'; import { writeTmpFile } from '../lib/util'; import { Command } from '../types/index'; diff --git a/src/commands/inventory.ts b/src/commands/inventory.ts index b8cf6b4..f50116e 100644 --- a/src/commands/inventory.ts +++ b/src/commands/inventory.ts @@ -1,6 +1,7 @@ import { GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js'; import { ItemInventory, db } from '../lib/db'; import { formatItems, getItem } from '../lib/rpg/items'; +import { initHealth } from '../lib/rpg/pvp'; import { Command } from '../types/index'; export default { @@ -14,6 +15,8 @@ export default { const member = interaction.member! as GuildMember; + await initHealth(member.id); + await interaction.deferReply({ephemeral: true}); const itemsList = await db('itemInventories') diff --git a/src/commands/put.ts b/src/commands/put.ts index 5fe441b..0e02a3f 100644 --- a/src/commands/put.ts +++ b/src/commands/put.ts @@ -1,6 +1,7 @@ import { GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js'; import { changeLinkedCounterInteraction, linkedCounterAutocomplete } from '../lib/rpg/counter'; import { Command } from '../types/index'; +import { initHealth } from '../lib/rpg/pvp'; export default { data: new SlashCommandBuilder() @@ -27,6 +28,8 @@ export default { const member = interaction.member! as GuildMember; + await initHealth(member.id); + const amount = Math.trunc(interaction.options.getInteger('amount') || 1); const type = interaction.options.getString('type')!; diff --git a/src/commands/take.ts b/src/commands/take.ts index 37e3c27..31c22ce 100644 --- a/src/commands/take.ts +++ b/src/commands/take.ts @@ -1,5 +1,6 @@ import { GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js'; import { changeLinkedCounterInteraction, linkedCounterAutocomplete } from '../lib/rpg/counter'; +import { initHealth } from '../lib/rpg/pvp'; import { Command } from '../types/index'; export default { @@ -27,6 +28,8 @@ export default { const member = interaction.member! as GuildMember; + await initHealth(member.id); + const amount = Math.trunc(interaction.options.getInteger('amount') || 1); const type = interaction.options.getString('type')!; diff --git a/src/lib/rpg/items.ts b/src/lib/rpg/items.ts index 4fbd118..c48cb2d 100644 --- a/src/lib/rpg/items.ts +++ b/src/lib/rpg/items.ts @@ -1,6 +1,5 @@ import { AutocompleteInteraction } from 'discord.js'; import { CustomItem, ItemInventory, db } from '../db'; -import { MAX_HEALTH } from './pvp'; export type DefaultItem = Omit; // uses negative IDs export type Item = DefaultItem | CustomItem; @@ -118,7 +117,7 @@ export const defaultItems: DefaultItem[] = [ description: 'ow', emoji: '🩸', type: 'plain', - maxStack: MAX_HEALTH, + maxStack: 50, untradable: false }, { @@ -374,18 +373,23 @@ export function formatItemsArray(items: Items[], disableBold = false) { return items.map(i => formatItems(i.item, i.quantity, disableBold)).join(' '); } -function createItemAutocomplete(onlyCustom: boolean) { +function createItemAutocomplete(onlyCustom: boolean, filterType: 'plain' | 'weapon' | 'consumable' | null) { return async (interaction: AutocompleteInteraction) => { const focused = interaction.options.getFocused(); - const customItems = await db('customItems') + const itemQuery = db('customItems') .select('emoji', 'name', 'id') // @ts-expect-error this LITERALLY works .whereLike(db.raw('UPPER(name)'), `%${focused.toUpperCase()}%`) .where('guild', interaction.guildId!) .limit(25); + + if (filterType) itemQuery.where('type', filterType); + + const customItems = await itemQuery; - const 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); let items; if (onlyCustom) { @@ -400,5 +404,8 @@ function createItemAutocomplete(onlyCustom: boolean) { }; } -export const itemAutocomplete = createItemAutocomplete(false); -export const customItemAutocomplete = createItemAutocomplete(true); \ No newline at end of file +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 diff --git a/src/lib/rpg/pvp.ts b/src/lib/rpg/pvp.ts index 87e59f6..5bd5161 100644 --- a/src/lib/rpg/pvp.ts +++ b/src/lib/rpg/pvp.ts @@ -1,7 +1,11 @@ -import { InitHealth, db } from '../db'; -import { DefaultItems, getDefaultItem, giveItem } from './items'; +import { InitHealth, ItemInventory, db } from '../db'; +import { DefaultItems, getDefaultItem, giveItem, getItemQuantity, formatItems } from './items'; +import { Client } from 'discord.js'; -export const MAX_HEALTH = 100; +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 async function initHealth(user: string) { const isInitialized = await db('initHealth') @@ -9,7 +13,37 @@ export async function initHealth(user: string) { .first(); if (!isInitialized) { - giveItem(user, getDefaultItem(DefaultItems.BLOOD), MAX_HEALTH); + giveItem(user, BLOOD_ITEM, MAX_HEALTH); await db('initHealth').insert({ user }); } +} + +export async function getHealth(user: string) { + return await getItemQuantity(user, BLOOD_ID); +} + +export async function dealDamage(user: string, dmg: number) { + return await giveItem(user, BLOOD_ITEM, -dmg); +} + +async function healthCron(bot: Client) { + await db('itemInventories') + .where('item', BLOOD_ID) + .update({ + quantity: db.raw('MIN(quantity + ?, ?)', [BLOOD_GAIN_PER_HOUR, MAX_HEALTH]) + }); + + const debtedUsers = await db('itemInventories') + .select('user', 'quantity') + .where('quantity', '<', '0'); + + for (const debted of debtedUsers) { + const user = await bot.users.fetch(debted.user); + if (!user) continue; + await user.send(`${formatItems(BLOOD_ITEM, debted.quantity)} You are bleeding out to death`); + } +} + +export function init(bot: Client) { + setInterval(() => healthCron(bot), 1_000 * 60 * 60); } \ No newline at end of file