From fc46176044e3c5e07d6c06290d6bcc940a6fd7c9 Mon Sep 17 00:00:00 2001 From: Ayden Jahola Date: Sat, 26 Oct 2024 03:40:13 +0100 Subject: [PATCH] add economy system --- commands/economy/balance.js | 35 +++++++ commands/economy/daily.js | 72 +++++++++++++++ commands/economy/inventory.js | 70 ++++++++++++++ commands/economy/shop.js | 108 ++++++++++++++++++++++ commands/economy/trade.js | 166 ++++++++++++++++++++++++++++++++++ commands/economy/work.js | 70 ++++++++++++++ index.js | 4 + models/ShopItem.js | 10 ++ models/Trade.js | 12 +++ models/UserEconomy.js | 11 +++ models/UserInventory.js | 10 ++ utils/seedShopItems.js | 87 ++++++++++++++++++ 12 files changed, 655 insertions(+) create mode 100644 commands/economy/balance.js create mode 100644 commands/economy/daily.js create mode 100644 commands/economy/inventory.js create mode 100644 commands/economy/shop.js create mode 100644 commands/economy/trade.js create mode 100644 commands/economy/work.js create mode 100644 models/ShopItem.js create mode 100644 models/Trade.js create mode 100644 models/UserEconomy.js create mode 100644 models/UserInventory.js create mode 100644 utils/seedShopItems.js diff --git a/commands/economy/balance.js b/commands/economy/balance.js new file mode 100644 index 0000000..e65e355 --- /dev/null +++ b/commands/economy/balance.js @@ -0,0 +1,35 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); +const UserEconomy = require("../../models/UserEconomy"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("balance") + .setDescription("Check your balance"), + + async execute(interaction) { + const { user, guild } = interaction; + + let userEconomy = await UserEconomy.findOne({ + userId: user.id, + guildId: guild.id, + }); + + if (!userEconomy) { + userEconomy = await UserEconomy.create({ + userId: user.id, + guildId: guild.id, + }); + } + + const embed = new EmbedBuilder() + .setColor("#0099ff") + .setTitle(`${user.username}'s Balance`) + .setDescription(`You have **${userEconomy.balance}** coins.`) + .setTimestamp() + .setFooter({ + text: `Requested by ${user.username}`, + iconURL: user.displayAvatarURL(), + }); + await interaction.reply({ embeds: [embed] }); + }, +}; diff --git a/commands/economy/daily.js b/commands/economy/daily.js new file mode 100644 index 0000000..c6772ba --- /dev/null +++ b/commands/economy/daily.js @@ -0,0 +1,72 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); +const UserEconomy = require("../../models/UserEconomy"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("daily") + .setDescription("Claim your daily reward"), + + async execute(interaction) { + const { user, guild } = interaction; + const dailyReward = 100; + const oneDay = 24 * 60 * 60 * 1000; + + let userEconomy = await UserEconomy.findOne({ + userId: user.id, + guildId: guild.id, + }); + + if (!userEconomy) { + userEconomy = await UserEconomy.create({ + userId: user.id, + guildId: guild.id, + }); + } + + const now = new Date(); + if (userEconomy.lastDaily && now - userEconomy.lastDaily < oneDay) { + const remainingTime = + new Date(userEconomy.lastDaily.getTime() + oneDay) - now; + const hours = Math.floor((remainingTime / (1000 * 60 * 60)) % 24); + const minutes = Math.floor((remainingTime / (1000 * 60)) % 60); + const seconds = Math.floor((remainingTime / 1000) % 60); + + const errorEmbed = new EmbedBuilder() + .setColor("#ff0000") + .setTitle("Daily Reward Claim") + .setDescription( + `You have already claimed your daily reward today. Come back in **${hours}h ${minutes}m ${seconds}s**!` + ) + .setFooter({ + text: `Requested in ${guild.name}`, + iconURL: guild.iconURL() || null, + }); + + await interaction.reply({ embeds: [errorEmbed] }); + return; + } + + userEconomy.balance += dailyReward; + userEconomy.lastDaily = now; + await userEconomy.save(); + + const successEmbed = new EmbedBuilder() + .setColor("#00ff00") + .setTitle("Daily Reward Claimed!") + .setDescription( + `You claimed your daily reward of **${dailyReward}** coins!` + ) + .addFields({ + name: "Total Balance", + value: `You now have **${userEconomy.balance}** coins.`, + inline: true, + }) + .setTimestamp() + .setFooter({ + text: `Requested by ${user.username}`, + iconURL: user.displayAvatarURL(), + }); + + await interaction.reply({ embeds: [successEmbed] }); + }, +}; diff --git a/commands/economy/inventory.js b/commands/economy/inventory.js new file mode 100644 index 0000000..0739d48 --- /dev/null +++ b/commands/economy/inventory.js @@ -0,0 +1,70 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); +const UserInventory = require("../../models/UserInventory"); +const ShopItem = require("../../models/ShopItem"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("inventory") + .setDescription("View your inventory"), + + async execute(interaction) { + const { user, guild } = interaction; + + const inventory = await UserInventory.find({ + userId: user.id, + guildId: guild.id, + }); + + if (inventory.length === 0) { + const emptyEmbed = new EmbedBuilder() + .setColor("#ff0000") + .setTitle("Inventory") + .setDescription("Your inventory is empty.") + .setFooter({ + text: `Requested in ${guild.name}`, + iconURL: guild.iconURL() || null, + }); + + await interaction.reply({ embeds: [emptyEmbed] }); + return; + } + + const itemDetails = await Promise.all( + inventory.map(async (item) => { + const shopItem = await ShopItem.findOne({ itemId: item.itemId }); + if (item.quantity > 0) { + return `${shopItem.name} (x${item.quantity})`; + } + return null; + }) + ); + + const filteredItemDetails = itemDetails.filter((detail) => detail !== null); + + if (filteredItemDetails.length === 0) { + const noItemsEmbed = new EmbedBuilder() + .setColor("#ff0000") + .setTitle("Inventory") + .setDescription("Your inventory is empty.") + .setFooter({ + text: `Requested in ${guild.name}`, + iconURL: guild.iconURL() || null, + }); + + await interaction.reply({ embeds: [noItemsEmbed] }); + return; + } + + const inventoryEmbed = new EmbedBuilder() + .setColor("#00ff00") + .setTitle(`${user.username}'s Inventory`) + .setDescription(filteredItemDetails.join("\n")) + .setTimestamp() + .setFooter({ + text: `Requested by ${user.username}`, + iconURL: user.displayAvatarURL(), + }); + + await interaction.reply({ embeds: [inventoryEmbed] }); + }, +}; diff --git a/commands/economy/shop.js b/commands/economy/shop.js new file mode 100644 index 0000000..797abbf --- /dev/null +++ b/commands/economy/shop.js @@ -0,0 +1,108 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); +const ShopItem = require("../../models/ShopItem"); +const UserEconomy = require("../../models/UserEconomy"); +const UserInventory = require("../../models/UserInventory"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("shop") + .setDescription("View the shop and buy items") + .addStringOption((option) => + option.setName("item").setDescription("The ID of the item to buy") + ), + + async execute(interaction) { + const { user, guild } = interaction; + const itemId = interaction.options.getString("item"); + + if (!itemId) { + const items = await ShopItem.find(); + const itemDescriptions = items.map( + (item) => `**${item.itemId}**: ${item.name} - **${item.price}** coins` + ); + + const shopEmbed = new EmbedBuilder() + .setColor("#00bfff") + .setTitle("🛒 Shop Items") + .setDescription( + itemDescriptions.length > 0 + ? itemDescriptions.join("\n") + : "No items available at the moment." + ) + .setFooter({ + text: `Requested in ${guild.name}`, + iconURL: guild.iconURL() || null, + }); + + await interaction.reply({ embeds: [shopEmbed] }); + return; + } + + const shopItem = await ShopItem.findOne({ itemId }); + if (!shopItem) { + const notFoundEmbed = new EmbedBuilder() + .setColor("#ff0000") + .setTitle("❌ Item Not Found") + .setDescription("The specified item could not be found in the shop.") + .setFooter({ + text: `Requested in ${guild.name}`, + iconURL: guild.iconURL() || null, + }); + + await interaction.reply({ embeds: [notFoundEmbed] }); + return; + } + + const userEconomy = await UserEconomy.findOne({ + userId: user.id, + guildId: guild.id, + }); + if (!userEconomy || userEconomy.balance < shopItem.price) { + const insufficientFundsEmbed = new EmbedBuilder() + .setColor("#ff0000") + .setTitle("💸 Insufficient Funds") + .setDescription("You don't have enough coins to purchase this item.") + .setFooter({ + text: `Requested in ${guild.name}`, + iconURL: guild.iconURL() || null, + }); + + await interaction.reply({ embeds: [insufficientFundsEmbed] }); + return; + } + + userEconomy.balance -= shopItem.price; + await userEconomy.save(); + + let userInventory = await UserInventory.findOne({ + userId: user.id, + guildId: guild.id, + itemId, + }); + if (userInventory) { + userInventory.quantity += 1; + } else { + userInventory = new UserInventory({ + userId: user.id, + guildId: guild.id, + itemId, + quantity: 1, + }); + } + await userInventory.save(); + + const successEmbed = new EmbedBuilder() + .setColor("#00ff00") + .setTitle("🎉 Purchase Successful") + .setDescription( + `You've successfully purchased **${shopItem.name}** for **${shopItem.price}** coins!` + ) + .setTimestamp() + .setFooter({ + text: `Requested by ${user.username}`, + iconURL: user.displayAvatarURL(), + }); + + await interaction.reply({ embeds: [successEmbed] }); + }, +}; diff --git a/commands/economy/trade.js b/commands/economy/trade.js new file mode 100644 index 0000000..3426b07 --- /dev/null +++ b/commands/economy/trade.js @@ -0,0 +1,166 @@ +const { SlashCommandBuilder } = require("discord.js"); +const UserInventory = require("../../models/UserInventory"); +const UserEconomy = require("../../models/UserEconomy"); +const Trade = require("../../models/Trade"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("trade") + .setDescription("Trade an item and/or coins with another user") + .addUserOption((option) => + option + .setName("user") + .setDescription("The user to trade with") + .setRequired(true) + ) + .addStringOption((option) => + option + .setName("item") + .setDescription("The ID of the item to trade") + .setRequired(true) + ) + .addIntegerOption((option) => + option + .setName("quantity") + .setDescription("Quantity of the item to trade") + .setRequired(true) + ) + .addIntegerOption((option) => + option + .setName("coins") + .setDescription("Amount of coins to trade") + .setRequired(false) + ), + + async execute(interaction) { + const { user, guild } = interaction; + const tradeUser = interaction.options.getUser("user"); + const itemId = interaction.options.getString("item"); + const quantity = interaction.options.getInteger("quantity"); + const coins = interaction.options.getInteger("coins") || 0; + + if (tradeUser.id === user.id) { + await interaction.reply("You can't trade items with yourself."); + return; + } + + const userInventory = await UserInventory.findOne({ + userId: user.id, + guildId: guild.id, + itemId, + }); + if (!userInventory || userInventory.quantity < quantity) { + await interaction.reply("You don't have enough of this item to trade."); + return; + } + + const tradeUserEconomy = await UserEconomy.findOne({ + userId: tradeUser.id, + guildId: guild.id, + }); + if (!tradeUserEconomy || tradeUserEconomy.balance < coins) { + await interaction.reply( + `${tradeUser.username} does not have enough coins to trade.` + ); + return; + } + + const tradeProposal = new Trade({ + from: user.id, + to: tradeUser.id, + itemId, + quantity, + coins, + }); + await tradeProposal.save(); + + await interaction.reply({ + content: `Trade proposed: You are trading **${quantity}** of **${itemId}** and **${coins}** coins with ${tradeUser.username}.`, + ephemeral: true, + }); + + await tradeUser.send( + `Trade proposal: You are being offered **${quantity}** of **${itemId}** and **${coins}** coins by ${user.username}. Type \`/accept ${tradeProposal._id}\` to accept or \`/reject ${tradeProposal._id}\` to reject the trade.` + ); + }, + + async accept(interaction) { + const tradeId = interaction.options.getString("tradeId"); + const tradeProposal = await Trade.findById(tradeId); + + if (!tradeProposal) { + await interaction.reply("Trade not found or already completed."); + return; + } + + const { from, to, itemId, quantity, coins } = tradeProposal; + + const fromInventory = await UserInventory.findOne({ + userId: from, + guildId: interaction.guild.id, + itemId, + }); + + if (!fromInventory || fromInventory.quantity < quantity) { + await interaction.reply( + "Trade cannot be completed because the item no longer exists in the sender's inventory." + ); + return; + } + + let toInventory = await UserInventory.findOne({ + userId: to, + guildId: interaction.guild.id, + itemId, + }); + if (toInventory) { + toInventory.quantity += quantity; + } else { + toInventory = new UserInventory({ + userId: to, + guildId: interaction.guild.id, + itemId, + quantity, + }); + } + await toInventory.save(); + + const fromEconomy = await UserEconomy.findOne({ + userId: from, + guildId: interaction.guild.id, + }); + const toEconomy = await UserEconomy.findOne({ + userId: to, + guildId: interaction.guild.id, + }); + + if (fromEconomy) { + fromEconomy.balance -= coins; + await fromEconomy.save(); + } + + if (toEconomy) { + toEconomy.balance += coins; + await toEconomy.save(); + } + + await Trade.deleteOne({ _id: tradeId }); + + await interaction.reply( + `Trade completed! You traded **${quantity}** of **${itemId}** and **${coins}** coins with ${interaction.user.username}.` + ); + }, + + async reject(interaction) { + const tradeId = interaction.options.getString("tradeId"); + const tradeProposal = await Trade.findById(tradeId); + + if (!tradeProposal) { + await interaction.reply("Trade not found or already completed."); + return; + } + + await Trade.deleteOne({ _id: tradeId }); + await interaction.reply(`Trade rejected.`); + }, +}; diff --git a/commands/economy/work.js b/commands/economy/work.js new file mode 100644 index 0000000..42e6afb --- /dev/null +++ b/commands/economy/work.js @@ -0,0 +1,70 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); +const UserEconomy = require("../../models/UserEconomy"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("work") + .setDescription("Work to earn coins!"), + + async execute(interaction) { + const { user, guild } = interaction; + const workReward = 100; + const cooldownTime = 60 * 60 * 1000; + + let userEconomy = await UserEconomy.findOne({ + userId: user.id, + guildId: guild.id, + }); + + if (!userEconomy) { + userEconomy = await UserEconomy.create({ + userId: user.id, + guildId: guild.id, + lastWork: null, + balance: 0, + }); + } + + const now = new Date(); + + if (userEconomy.lastWork && now - userEconomy.lastWork < cooldownTime) { + const remainingTime = cooldownTime - (now - userEconomy.lastWork); + const remainingMinutes = Math.ceil(remainingTime / (60 * 1000)); + + const cooldownEmbed = new EmbedBuilder() + .setColor("#ff0000") + .setTitle("Cooldown") + .setDescription( + `You need to wait **${remainingMinutes}** minutes before you can work again.` + ) + .setFooter({ + text: `Requested in ${guild.name}`, + iconURL: guild.iconURL() || null, + }); + + await interaction.reply({ embeds: [cooldownEmbed] }); + return; + } + + userEconomy.balance += workReward; + userEconomy.lastWork = now; + await userEconomy.save(); + + const successEmbed = new EmbedBuilder() + .setColor("#00ff00") + .setTitle("Work Success") + .setDescription(`You worked hard and earned **${workReward}** coins!`) + .addFields({ + name: "Total Balance", + value: `${userEconomy.balance} coins`, + inline: true, + }) + .setTimestamp() + .setFooter({ + text: `Requested by ${user.username}`, + iconURL: user.displayAvatarURL(), + }); + + await interaction.reply({ embeds: [successEmbed] }); + }, +}; diff --git a/index.js b/index.js index c2b756f..b50e69b 100644 --- a/index.js +++ b/index.js @@ -10,6 +10,7 @@ const mongoose = require("mongoose"); const fs = require("fs"); const path = require("path"); const ServerSettings = require("./models/ServerSettings"); +const seedShopItems = require("./utils/seedShopItems"); const client = new Client({ intents: [ @@ -61,6 +62,9 @@ client.once("ready", async () => { console.log(`🤖 Logged in as ${client.user.tag}`); console.log(`==============================`); + // Seed the shop items + await seedShopItems(); + // Register commands for all existing guilds const guilds = client.guilds.cache.map((guild) => guild.id); for (const guildId of guilds) { diff --git a/models/ShopItem.js b/models/ShopItem.js new file mode 100644 index 0000000..ef48a61 --- /dev/null +++ b/models/ShopItem.js @@ -0,0 +1,10 @@ +const mongoose = require("mongoose"); + +const shopItemSchema = new mongoose.Schema({ + itemId: { type: String, required: true, unique: true }, + name: { type: String, required: true }, + price: { type: Number, required: true }, + description: { type: String, required: true }, +}); + +module.exports = mongoose.model("ShopItem", shopItemSchema); diff --git a/models/Trade.js b/models/Trade.js new file mode 100644 index 0000000..7329b2d --- /dev/null +++ b/models/Trade.js @@ -0,0 +1,12 @@ +const mongoose = require("mongoose"); + +const tradeSchema = new mongoose.Schema({ + from: { type: String, required: true }, + to: { type: String, required: true }, + itemId: { type: String, required: true }, + quantity: { type: Number, required: true }, + coins: { type: Number, required: true }, + createdAt: { type: Date, default: Date.now, expires: "1d" }, +}); + +module.exports = mongoose.model("Trade", tradeSchema); diff --git a/models/UserEconomy.js b/models/UserEconomy.js new file mode 100644 index 0000000..d269140 --- /dev/null +++ b/models/UserEconomy.js @@ -0,0 +1,11 @@ +const mongoose = require("mongoose"); + +const userEconomySchema = new mongoose.Schema({ + userId: { type: String, required: true, unique: true }, + guildId: { type: String, required: true }, + balance: { type: Number, default: 200 }, + lastDaily: { type: Date, default: null }, + lastWork: { type: Date, default: null }, +}); + +module.exports = mongoose.model("UserEconomy", userEconomySchema); diff --git a/models/UserInventory.js b/models/UserInventory.js new file mode 100644 index 0000000..c6756b0 --- /dev/null +++ b/models/UserInventory.js @@ -0,0 +1,10 @@ +const mongoose = require("mongoose"); + +const userInventorySchema = new mongoose.Schema({ + userId: { type: String, required: true }, + guildId: { type: String, required: true }, + itemId: { type: String, required: true }, + quantity: { type: Number, default: 1 }, +}); + +module.exports = mongoose.model("UserInventory", userInventorySchema); diff --git a/utils/seedShopItems.js b/utils/seedShopItems.js new file mode 100644 index 0000000..4a80d4a --- /dev/null +++ b/utils/seedShopItems.js @@ -0,0 +1,87 @@ +const ShopItem = require("../models/ShopItem"); + +async function seedShopItems() { + const items = [ + // Valorant Skins + { + itemId: "prime_vandal", + name: "Prime Vandal", + price: 1200, + description: + "A futuristic skin for the Vandal with a sleek design and special effects.", + }, + { + itemId: "reaver_vandal", + name: "Reaver Vandal", + price: 1500, + description: + "One of the most popular Vandal skins with a haunting aesthetic and special animations.", + }, + { + itemId: "sovereign_ghost", + name: "Sovereign Ghost", + price: 800, + description: + "Golden elegance for the Ghost pistol with unique sound effects.", + }, + { + itemId: "araxys_operator", + name: "Araxys Operator", + price: 2000, + description: + "A top-tier sniper skin with alien-like animations and sound effects.", + }, + { + itemId: "glitchpop_bulldog", + name: "Glitchpop Bulldog", + price: 900, + description: + "A flashy skin for the Bulldog with vibrant colors and cyberpunk vibe.", + }, + + // CS2 Skins + { + itemId: "dragon_lore_awp", + name: "AWP Dragon Lore", + price: 2500, + description: + "A legendary skin for the AWP with dragon designs, a rare and coveted item.", + }, + { + itemId: "ak47_redline", + name: "AK-47 Redline", + price: 1000, + description: + "A simple yet iconic AK-47 skin with red and black color scheme.", + }, + { + itemId: "m4a4_howl", + name: "M4A4 Howl", + price: 2200, + description: + "A rare and valuable skin for the M4A4 with a striking wolf design.", + }, + { + itemId: "desert_eagle_kumicho_dragon", + name: "Desert Eagle Kumicho Dragon", + price: 800, + description: + "A Desert Eagle skin with an intricate dragon design and a metallic finish.", + }, + { + itemId: "usp_kill_confirmed", + name: "USP-S Kill Confirmed", + price: 1100, + description: + "A detailed skin for the USP-S with a unique comic-style design.", + }, + ]; + + for (const item of items) { + await ShopItem.updateOne({ itemId: item.itemId }, item, { upsert: true }); + } + + console.log("✅ Shop items seeded!"); +} + +module.exports = seedShopItems;