From 2427cb1d9415e8be5c8ee1e7dd6e7af98f036021 Mon Sep 17 00:00:00 2001 From: TheMonDon <11539895+TheMonDon@users.noreply.github.com> Date: Thu, 25 Jul 2024 12:34:05 -0500 Subject: [PATCH] Initial Commit of Items category --- commands/Economy/rob.js | 4 +- commands/General/help.js | 1 + commands/Items/buy-item.js | 66 +++++++++++++++++ commands/Items/create-item.js | 100 +++++++++++++++++++++++++ commands/Items/delete-item.js | 46 ++++++++++++ commands/Items/edit-item.js | 84 +++++++++++++++++++++ commands/Items/inventory.js | 131 ++++++++++++++++++++++++++++++++ commands/Items/item-info.js | 45 +++++++++++ commands/Items/store.js | 136 ++++++++++++++++++++++++++++++++++ 9 files changed, 611 insertions(+), 2 deletions(-) create mode 100644 commands/Items/buy-item.js create mode 100644 commands/Items/create-item.js create mode 100644 commands/Items/delete-item.js create mode 100644 commands/Items/edit-item.js create mode 100644 commands/Items/inventory.js create mode 100644 commands/Items/item-info.js create mode 100644 commands/Items/store.js diff --git a/commands/Economy/rob.js b/commands/Economy/rob.js index 1e1c37f0..03c5caf2 100755 --- a/commands/Economy/rob.js +++ b/commands/Economy/rob.js @@ -83,12 +83,12 @@ class Rob extends Command { let totalAmount = Number(memCash + authNet); if (!Number.isFinite(totalAmount)) { - totalAmount = Number.MAX_SAFE_INTEGER; + totalAmount = Number.MAX_VALUE; } let authNetAmount = Number(authNet); if (!Number.isFinite(authNetAmount)) { - authNetAmount = Number.MAX_SAFE_INTEGER; + authNetAmount = Number.MAX_VALUE; } const failRate = Math.floor((authNetAmount / totalAmount) * (maxRate - minRate + 1) + minRate); diff --git a/commands/General/help.js b/commands/General/help.js index 4f6b502d..95b87e6c 100644 --- a/commands/General/help.js +++ b/commands/General/help.js @@ -16,6 +16,7 @@ class Help extends Command { async run(msg, args, level) { const baseCategories = [ 'economy', + 'items', 'fun', 'games', 'general', diff --git a/commands/Items/buy-item.js b/commands/Items/buy-item.js new file mode 100644 index 00000000..636a0c8a --- /dev/null +++ b/commands/Items/buy-item.js @@ -0,0 +1,66 @@ +const Command = require('../../base/Command.js'); +const { EmbedBuilder } = require('discord.js'); +const { QuickDB } = require('quick.db'); +const db = new QuickDB(); + +class BuyItem extends Command { + constructor(client) { + super(client, { + name: 'buy-item', + category: 'Items', + description: 'Buy an item from the store.', + usage: 'buy-item ', + examples: ['buy pizza'], + aliases: ['buy'], + requiredArgs: 1, + guildOnly: true, + }); + } + + async run(msg, args) { + const itemName = args.join(' ').toLowerCase(); + if (!itemName) return msg.reply('Please specify an item to buy.'); + + const store = (await db.get(`servers.${msg.guild.id}.economy.store`)) || {}; + + // Find the item in the store regardless of case + const itemKey = Object.keys(store).find((key) => key.toLowerCase() === itemName); + + if (!itemKey) return msg.reply('That item does not exist in the store.'); + + const item = store[itemKey]; + const itemCost = BigInt(item.cost); + let userCash = BigInt(await db.get(`servers.${msg.guild.id}.users.${msg.member.id}.economy.cash`)); + if (userCash < itemCost) return msg.reply('You do not have enough money to buy this item.'); + + const userInventory = (await db.get(`servers.${msg.guild.id}.users.${msg.member.id}.economy.inventory`)) || []; + + // Check if the user already owns the item + const alreadyOwned = userInventory.find((inventoryItem) => inventoryItem.name.toLowerCase() === itemName); + if (alreadyOwned) return msg.reply('You already own this item.'); + + // Add the item to the user's inventory + userInventory.push({ name: itemKey, ...item }); + + // Deduct the cost from the user's cash + userCash = userCash - itemCost; + await db.set(`servers.${msg.guild.id}.users.${msg.member.id}.economy.cash`, userCash.toString()); + await db.set(`servers.${msg.guild.id}.users.${msg.member.id}.economy.inventory`, userInventory); + + const currencySymbol = (await db.get(`servers.${msg.guild.id}.economy.symbol`)) || '$'; + const csCost = + itemCost.toLocaleString().length > 700 + ? currencySymbol + itemCost.toLocaleString().slice(0, 700) + '...' + : currencySymbol + itemCost.toLocaleString(); + + const embed = new EmbedBuilder() + .setTitle('Purchase Successful') + .setDescription(`You have successfully bought **${itemKey}** for ${csCost}.`) + .setColor(msg.settings.embedColor) + .setTimestamp(); + + return msg.channel.send({ embeds: [embed] }); + } +} + +module.exports = BuyItem; diff --git a/commands/Items/create-item.js b/commands/Items/create-item.js new file mode 100644 index 00000000..617adea2 --- /dev/null +++ b/commands/Items/create-item.js @@ -0,0 +1,100 @@ +const Command = require('../../base/Command.js'); +const { EmbedBuilder } = require('discord.js'); +const { QuickDB } = require('quick.db'); +const db = new QuickDB(); + +class CreateItem extends Command { + constructor(client) { + super(client, { + name: 'create-item', + category: 'Items', + description: 'Create an item to be shown in the store.', + usage: 'create-item ', + longDescription: 'Create an item to be shown in the store. The item name will be cut off at 200 characters', + aliases: ['createitem'], + permLevel: 'Administrator', + guildOnly: true, + requiredArgs: 1, + }); + } + + async run(msg, args) { + const name = args.join(' ').slice(0, 200); + const store = (await db.get(`servers.${msg.guild.id}.economy.store`)) || {}; + const filter = (m) => m.author.id === msg.author.id; + const embed = new EmbedBuilder() + .setAuthor({ name: msg.author.tag, iconURL: msg.author.displayAvatarURL() }) + .setColor(msg.settings.embedColor) + .addFields([{ name: 'Name', value: name, inline: true }]) + .setFooter({ text: 'Type cancel to quit.' }) + .setTimestamp(); + + // Find the item in the store regardless of case + const item = Object.keys(store).find((key) => key.toLowerCase() === name); + if (item) { + const noItemEmbed = new EmbedBuilder() + .setAuthor({ name: msg.author.tag, iconURL: msg.author.displayAvatarURL() }) + .setColor(msg.settings.embedErrorColor) + .setDescription('There is already an item with that name.'); + + return msg.channel.send({ embeds: [noItemEmbed] }); + } + + const message = await msg.channel.send({ content: 'What would you like the price to be?', embeds: [embed] }); + + let collected = await msg.channel + .awaitMessages({ + filter, + max: 1, + time: 60000, + errors: ['time'], + }) + .catch(() => null); + if (!collected) return msg.reply('You did not reply in time, the command has been cancelled.'); + if (collected.first().content.toLowerCase() === 'cancel') return msg.reply('The command has been cancelled.'); + + let cost = parseInt( + collected + .first() + .content.toLowerCase() + .replace(/[^0-9\\.]/g, ''), + ); + if (isNaN(cost)) return msg.reply('The cost must be a number. Command has been cancelled.'); + if (cost === Infinity) { + msg.reply(`The cost must be less than Infinity. The cost has been set to ${Number.MAX_VALUE.toLocaleString()}.`); + cost = Number.MAX_VALUE; + } + if (cost < 0) { + msg.reply('The cost must be at least zero, therefore the cost has been set to zero.'); + cost = 0; + } + + const currencySymbol = (await db.get(`servers.${msg.guild.id}.economy.symbol`)) || '$'; + embed.addFields([{ name: 'Cost', value: currencySymbol + cost.toLocaleString(), inline: true }]); + + await message.edit({ content: 'What would you like the description to be?', embeds: [embed] }); + collected = await msg.channel + .awaitMessages({ + filter, + max: 1, + time: 60000, + errors: ['time'], + }) + .catch(() => null); + if (!collected) return msg.reply('You did not reply in time, the command has been cancelled.'); + if (collected.first().content.toLowerCase() === 'cancel') return msg.reply('The command has been cancelled.'); + + const description = collected.first().content.slice(0, 1024); + embed.addFields([{ name: 'Description', value: description, inline: true }]); + + store[name] = { + cost, + description, + }; + + await db.set(`servers.${msg.guild.id}.economy.store`, store); + return message.edit({ content: ':white_check_mark: Item created successfully!', embeds: [embed] }); + } +} + +module.exports = CreateItem; diff --git a/commands/Items/delete-item.js b/commands/Items/delete-item.js new file mode 100644 index 00000000..fda44a07 --- /dev/null +++ b/commands/Items/delete-item.js @@ -0,0 +1,46 @@ +const Command = require('../../base/Command.js'); +const { EmbedBuilder } = require('discord.js'); +const { QuickDB } = require('quick.db'); +const db = new QuickDB(); + +class DeleteItem extends Command { + constructor(client) { + super(client, { + name: 'delete-item', + category: 'Items', + description: 'Delete an item from the store.', + usage: 'delete-item ', + aliases: ['deleteitem', 'delitem'], + permLevel: 'Administrator', + guildOnly: true, + requiredArgs: 1, + }); + } + + async run(msg, args) { + const itemName = args.join(' ').toLowerCase(); + const store = (await db.get(`servers.${msg.guild.id}.economy.store`)) || {}; + + // Find the item in the store regardless of case + const itemKey = Object.keys(store).find((key) => key.toLowerCase() === itemName); + + const item = store[itemKey]; + if (!item) { + const embed = new EmbedBuilder() + .setAuthor({ name: msg.author.tag, iconURL: msg.author.displayAvatarURL() }) + .setColor(msg.settings.embedErrorColor) + .setDescription('There is not an item with that name.'); + + return msg.channel.send({ embeds: [embed] }); + } + + await db.delete(`servers.${msg.guild.id}.economy.store.${itemKey}`); + const embed = new EmbedBuilder() + .setColor(msg.settings.embedColor) + .setAuthor({ name: msg.author.tag, iconURL: msg.author.displayAvatarURL() }) + .setDescription('Item has been removed from the store.'); + return msg.channel.send({ embeds: [embed] }); + } +} + +module.exports = DeleteItem; diff --git a/commands/Items/edit-item.js b/commands/Items/edit-item.js new file mode 100644 index 00000000..1362ce39 --- /dev/null +++ b/commands/Items/edit-item.js @@ -0,0 +1,84 @@ +/* eslint-disable no-case-declarations */ +const Command = require('../../base/Command.js'); +const { EmbedBuilder } = require('discord.js'); +const { QuickDB } = require('quick.db'); +const db = new QuickDB(); + +class EditItem extends Command { + constructor(client) { + super(client, { + name: 'edit-item', + category: 'Items', + description: 'Edit an item in the store.', + usage: 'edit-item "" ', + aliases: ['edititem'], + permLevel: 'Administrator', + guildOnly: true, + requiredArgs: 3, + }); + } + + async run(msg, args) { + const attribute = args.shift().toLowerCase(); + const itemNameStartIndex = args.findIndex((arg) => arg.startsWith('"')); + const itemNameEndIndex = args.findIndex((arg) => arg.endsWith('"')); + + if (itemNameStartIndex === -1 || itemNameEndIndex === -1) { + return msg.reply('Please enclose the item name in double quotes.'); + } + + const itemName = args + .slice(itemNameStartIndex, itemNameEndIndex + 1) + .join(' ') + .replace(/"/g, ''); + const newValue = args.slice(itemNameEndIndex + 1).join(' '); + + const store = (await db.get(`servers.${msg.guild.id}.economy.store`)) || {}; + + // Find the item in the store regardless of case + const itemKey = Object.keys(store).find((key) => key.toLowerCase() === itemName.toLowerCase()); + + if (!itemKey) return msg.reply('That item does not exist in the store.'); + + const item = store[itemKey]; + + switch (attribute) { + case 'name': + // Ensure the new name is not already taken + const newItemKey = newValue.toLowerCase(); + if (Object.keys(store).find((key) => key.toLowerCase() === newItemKey)) { + return msg.reply('An item with that name already exists.'); + } + // Update the item name + store[newValue] = item; + delete store[itemKey]; + break; + case 'price': + const price = parseInt(newValue, 10); + if (isNaN(price) || price < 0) { + return msg.reply('Please provide a valid price.'); + } + item.cost = price; + store[itemKey] = item; + break; + case 'description': + item.description = newValue; + store[itemKey] = item; + break; + default: + return msg.reply('Invalid attribute. You can only edit name, price, or description.'); + } + + await db.set(`servers.${msg.guild.id}.economy.store`, store); + + const embed = new EmbedBuilder() + .setTitle('Item Edited') + .setDescription(`The **${attribute}** of **${itemKey}** has been updated to **${newValue}**.`) + .setColor(msg.settings.embedColor) + .setTimestamp(); + + return msg.channel.send({ embeds: [embed] }); + } +} + +module.exports = EditItem; diff --git a/commands/Items/inventory.js b/commands/Items/inventory.js new file mode 100644 index 00000000..c7af7477 --- /dev/null +++ b/commands/Items/inventory.js @@ -0,0 +1,131 @@ +const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); +const Command = require('../../base/Command.js'); +const { QuickDB } = require('quick.db'); +const db = new QuickDB(); + +class Inventory extends Command { + constructor(client) { + super(client, { + name: 'inventory', + category: 'Items', + description: "View yours or somebody else's inventory", + usage: 'inventory [member] [page]', + aliases: ['inv'], + guildOnly: true, + }); + } + + async run(msg, args) { + let mem = msg.member; + let page = 1; + if (args.length === 2) { + mem = await this.client.util.getMember(msg, args[0]); + page = parseInt(args[1]?.replace(/[^0-9\\.]/g, '') || 1); + } else if (args.length === 1) { + if (!parseInt(args[0])) { + mem = await this.client.util.getMember(msg, args[0]); + } else { + page = parseInt(args[0]?.replace(/[^0-9\\.]/g, '') || 1); + } + } + mem = mem.user ? mem.user : mem; + + const userInventory = (await db.get(`servers.${msg.guild.id}.users.${mem.id}.economy.inventory`)) || []; + const itemsPerPage = 10; // Number of items per page + const maxPages = Math.ceil(userInventory.length / itemsPerPage) || 1; + + // Ensure page is within valid range + page = Math.max(1, Math.min(page, maxPages)); + + const start = (page - 1) * itemsPerPage; + const end = start + itemsPerPage; + const paginatedInventory = userInventory.slice(start, end); + + const inventoryDetails = paginatedInventory + .map((item) => { + return `**${item.name}** - ${BigInt(item.cost || 0)?.toLocaleString()}\n${item.description}`; + }) + .join('\n'); + + const embed = new EmbedBuilder() + .setAuthor({ name: `${mem.username}'s Inventory`, iconURL: mem.displayAvatarURL() }) + .setColor(msg.settings.embedColor) + .setDescription(inventoryDetails || 'Nothing to see here!') + .setFooter({ text: `Page ${page} / ${maxPages}` }) + .setTimestamp(); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('prev_page') + .setLabel('Previous') + .setStyle(ButtonStyle.Primary) + .setDisabled(page === 1), + new ButtonBuilder() + .setCustomId('next_page') + .setLabel('Next') + .setStyle(ButtonStyle.Primary) + .setDisabled(page === maxPages), + ); + + const message = await msg.channel.send({ embeds: [embed], components: [row] }); + const collector = message.createMessageComponentCollector({ time: 2147483647 }); + + collector.on('collect', async (interaction) => { + if (interaction.user.id !== msg.author.id) { + return interaction.reply({ content: 'These buttons are not for you!', ephemeral: true }); + } + + if (interaction.customId === 'prev_page') page--; + if (interaction.customId === 'next_page') page++; + + // Ensure page is within valid range + page = Math.max(1, Math.min(page, maxPages)); + + const newStart = (page - 1) * itemsPerPage; + const newEnd = newStart + itemsPerPage; + const newPaginatedInventory = userInventory.slice(newStart, newEnd); + + const newInventoryDetails = newPaginatedInventory + .map((item) => { + return `**${item.name}** - ${BigInt(item.cost || 0)?.toLocaleString()}\n${item.description}`; + }) + .join('\n'); + + const updatedEmbed = new EmbedBuilder() + .setColor(msg.settings.embedColor) + .setAuthor({ name: `${mem.username}'s Inventory`, iconURL: mem.displayAvatarURL() }) + .setDescription(newInventoryDetails || 'Nothing to see here!') + .setFooter({ text: `Page ${page} / ${maxPages}` }) + .setTimestamp(); + + const updatedRow = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('prev_page') + .setLabel('Previous') + .setStyle(ButtonStyle.Primary) + .setDisabled(page === 1), + new ButtonBuilder() + .setCustomId('next_page') + .setLabel('Next') + .setStyle(ButtonStyle.Primary) + .setDisabled(page === maxPages), + ); + + await interaction.update({ embeds: [updatedEmbed], components: [updatedRow] }); + }); + + collector.on('end', () => { + const disabledRow = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('prev_page') + .setLabel('Previous') + .setStyle(ButtonStyle.Primary) + .setDisabled(true), + new ButtonBuilder().setCustomId('next_page').setLabel('Next').setStyle(ButtonStyle.Primary).setDisabled(true), + ); + message.edit({ components: [disabledRow] }); + }); + } +} + +module.exports = Inventory; diff --git a/commands/Items/item-info.js b/commands/Items/item-info.js new file mode 100644 index 00000000..41b6122c --- /dev/null +++ b/commands/Items/item-info.js @@ -0,0 +1,45 @@ +const Command = require('../../base/Command.js'); +const { EmbedBuilder } = require('discord.js'); +const { QuickDB } = require('quick.db'); +const db = new QuickDB(); + +class ItemInfo extends Command { + constructor(client) { + super(client, { + name: 'item-info', + category: 'Items', + description: 'View information about an item.', + usage: 'item-info ', + aliases: ['iteminfo'], + guildOnly: true, + requiredArgs: 1, + }); + } + + async run(msg, args) { + const itemName = args.join(' ').toLowerCase(); + const store = (await db.get(`servers.${msg.guild.id}.economy.store`)) || {}; + + // Find the item in the store regardless of case + const itemKey = Object.keys(store).find((key) => key.toLowerCase() === itemName); + + const item = store[itemKey]; + if (!item) { + const embed = new EmbedBuilder() + .setAuthor({ name: msg.author.tag, iconURL: msg.author.displayAvatarURL() }) + .setColor(msg.settings.embedErrorColor) + .setDescription('There is not an item with that name.'); + + return msg.channel.send({ embeds: [embed] }); + } + + const currencySymbol = (await db.get(`servers.${msg.guild.id}.economy.symbol`)) || '$'; + const embed = new EmbedBuilder() + .setColor(msg.settings.embedColor) + .setAuthor({ name: msg.author.tag, iconURL: msg.author.displayAvatarURL() }) + .addFields([{ name: 'Cost', value: currencySymbol + BigInt(item.cost).toLocaleString(), inline: true }]); + return msg.channel.send({ embeds: [embed] }); + } +} + +module.exports = ItemInfo; diff --git a/commands/Items/store.js b/commands/Items/store.js new file mode 100644 index 00000000..53235103 --- /dev/null +++ b/commands/Items/store.js @@ -0,0 +1,136 @@ +const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); +const Command = require('../../base/Command.js'); +const { stripIndents } = require('common-tags'); +const { QuickDB } = require('quick.db'); + +const db = new QuickDB(); + +class Store extends Command { + constructor(client) { + super(client, { + name: 'store', + description: 'View the items available for purchase', + category: 'Items', + examples: ['store [page]'], + aliases: ['shop'], + usage: 'store [page]', + guildOnly: true, + }); + } + + async run(msg, args) { + let page = parseInt(args.join(' ')) || 1; + + if (isNaN(page)) return this.client.util.errorEmbed(msg, msg.settings.prefix + this.help.usage, 'Incorrect Usage'); + + await msg.guild.members.fetch(); + const currencySymbol = (await db.get(`servers.${msg.guild.id}.economy.symbol`)) || '$'; + const store = (await db.get(`servers.${msg.guild.id}.economy.store`)) || {}; + + // Construct the message with item names + const itemDetails = Object.entries(store).map(([itemName, itemInfo], index) => { + return { + display: `${index + 1}. **${itemName}** - ${currencySymbol}${itemInfo.cost.toLocaleString()}\n${ + itemInfo.description + }`, + }; + }); + + const maxPages = Math.ceil(itemDetails.length / 10) || 1; + + // Ensure page is within valid range + page = Math.max(1, Math.min(page, maxPages)); + + let displayedStore = itemDetails.slice((page - 1) * 10, page * 10); + + const embed = new EmbedBuilder() + .setColor(msg.settings.embedColor) + .setTitle(`${msg.guild.name} Store`) + .setAuthor({ name: msg.author.tag, iconURL: msg.author.displayAvatarURL() }) + .setDescription(`${displayedStore.map((entry) => entry.display).join('\n') || 'None'}`) + .setFooter({ text: `Page ${page} / ${maxPages}` }) + .setTimestamp(); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('prev_page') + .setLabel('Previous') + .setStyle(ButtonStyle.Primary) + .setDisabled(page === 1), + new ButtonBuilder() + .setCustomId('next_page') + .setLabel('Next') + .setStyle(ButtonStyle.Primary) + .setDisabled(page === maxPages), + ); + + if (!itemDetails.length) { + const errorEmbed = new EmbedBuilder() + .setColor(msg.settings.embedColor) + .setTitle(`${msg.guild.name} Store`) + .setAuthor({ name: msg.author.tag, iconURL: msg.author.displayAvatarURL() }) + .setDescription( + stripIndents` + The store is empty. Someone probably robbed it :shrug: + Add items to the store using the create-item command.`, + ) + .setFooter({ text: `Page ${page} / ${maxPages}` }); + + return await msg.channel.send({ embeds: [errorEmbed], components: [row] }); + } + + const message = await msg.channel.send({ embeds: [embed], components: [row] }); + const collector = message.createMessageComponentCollector({ time: 2147483647 }); + + collector.on('collect', async (interaction) => { + if (interaction.user.id !== msg.author.id) { + return interaction.reply({ content: 'These buttons are not for you!', ephemeral: true }); + } + + if (interaction.customId === 'prev_page') page--; + if (interaction.customId === 'next_page') page++; + + // Ensure page is within valid range + page = Math.max(1, Math.min(page, maxPages)); + + displayedStore = itemDetails.slice((page - 1) * 10, page * 10); + + const updatedEmbed = new EmbedBuilder() + .setColor(msg.settings.embedColor) + .setTitle(`${msg.guild.name} Store`) + .setAuthor({ name: msg.author.tag, iconURL: msg.author.displayAvatarURL() }) + .setDescription(`${displayedStore.map((entry) => entry.display).join('\n') || 'None'}`) + .setFooter({ text: `Page ${page} / ${maxPages}` }) + .setTimestamp(); + + const updatedRow = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('prev_page') + .setLabel('Previous') + .setStyle(ButtonStyle.Primary) + .setDisabled(page === 1), + new ButtonBuilder() + .setCustomId('next_page') + .setLabel('Next') + .setStyle(ButtonStyle.Primary) + .setDisabled(page === maxPages), + ); + + await interaction.update({ embeds: [updatedEmbed], components: [updatedRow] }); + }); + + collector.on('end', () => { + const disabledRow = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('prev_page') + .setLabel('Previous') + .setStyle(ButtonStyle.Primary) + .setDisabled(true), + new ButtonBuilder().setCustomId('next_page').setLabel('Next').setStyle(ButtonStyle.Primary).setDisabled(true), + ); + message.edit({ components: [disabledRow] }); + }); + } +} + +module.exports = Store;