const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); const { mClient } = require("../.."); const DATABASE = "neo-db"; const COOKIE_COLLECTION = "items_cookies"; const COOLDOWN_COLLECTION = "cooldowns_cookies"; const COLLECT_COOLDOWN = 60 * 60; // 1 hour const STEAL_COOLDOWN = 15 * 60; // 15 minutes const STEAL_SUCCESS_CHANCE = 0.50; const ABRA_ID = "378135175059603457"; const ABRA_MULTIPLIER = 1.5; module.exports = { data: new SlashCommandBuilder() .setName("cookies") .setDescription("The ShaiBot cookie economy.") .addSubcommand(subcommand => subcommand .setName("collect") .setDescription("Collect a fresh batch of cookies.") ) .addSubcommand(subcommand => subcommand .setName("steal") .setDescription("Attempt to steal another user's cookies.") .addUserOption(option => option .setName("target") .setDescription("The unfortunate victim.") .setRequired(true) ) ) .addSubcommand(subcommand => subcommand .setName("check") .setDescription("Check a cookie balance.") .addUserOption(option => option .setName("target") .setDescription("The user whose cookies you want to check.") ) ) .addSubcommand(subcommand => subcommand .setName("give") .setDescription("Give some of your cookies to another user.") .addUserOption(option => option .setName("target") .setDescription("The recipient.") .setRequired(true) ) .addIntegerOption(option => option .setName("amount") .setDescription("How many cookies to give.") .setRequired(true) .setMinValue(1) ) ) .addSubcommand(subcommand => subcommand .setName("leaderboard") .setDescription("Show the richest cookie collectors.") ), async execute(interaction) { switch (interaction.options.getSubcommand()) { case "collect": return collect(interaction); case "steal": return steal(interaction); case "check": return check(interaction); case "give": return give(interaction); case "leaderboard": return leaderboard(interaction); default: return interaction.reply({ content: "Unknown subcommand.", ephemeral: true }); } } }; /* * DATABASE */ function getCollections() { const db = mClient.db(DATABASE); return { cookies: db.collection(COOKIE_COLLECTION), cooldowns: db.collection(COOLDOWN_COLLECTION) }; } /* * COOKIE HELPERS */ async function getCookieBalance(userId) { const { cookies } = getCollections(); const result = await cookies.findOne( { userId }, { projection: { _id: 0, cookies: 1 } } ); return result?.cookies ?? 0; } async function changeCookies(userId, amount, session = undefined) { const { cookies } = getCollections(); return cookies.updateOne( { userId }, { $inc: { cookies: amount }, $setOnInsert: { userId } }, { upsert: true, session } ); } /* * COOKIE TRANSFER * * This uses a transaction so both balances update together. * * MongoDB transactions require Atlas or a replica set. */ async function transferCookies(fromUserId, toUserId, amount) { const { cookies } = getCollections(); const session = mClient.startSession(); try { await session.withTransaction(async () => { /* * Only debit if the sender still has enough cookies. * * This protects against two commands being executed * at almost the same time. */ const debit = await cookies.updateOne( { userId: fromUserId, cookies: { $gte: amount } }, { $inc: { cookies: -amount } }, { session } ); if (debit.modifiedCount !== 1) { throw new Error("INSUFFICIENT_COOKIES"); } await cookies.updateOne( { userId: toUserId }, { $inc: { cookies: amount }, $setOnInsert: { userId: toUserId } }, { upsert: true, session } ); }); } finally { await session.endSession(); } } /* * COOLDOWN HELPERS * * cooldowns_cookies: * * { * userId: "...", * guildId: "...", * collect: 1786741200, * steal: 1786738500 * } */ async function getCooldown(userId, guildId, type) { const { cooldowns } = getCollections(); const result = await cooldowns.findOne( { userId, guildId }, { projection: { _id: 0, [type]: 1 } } ); return result?.[type] ?? 0; } async function setCooldown(userId, guildId, type, duration) { const { cooldowns } = getCollections(); const now = getUnixTime(); const next = now + duration; await cooldowns.updateOne( { userId, guildId }, { $set: { [type]: next }, $setOnInsert: { userId, guildId } }, { upsert: true } ); return next; } /* * /cookies collect */ async function collect(interaction) { const userId = interaction.user.id; const guildId = interaction.guildId; const now = getUnixTime(); const cooldown = await getCooldown( userId, guildId, "collect" ); if (cooldown > now) { return interaction.reply({ content: `The batch is still cooking. Come back 🍪`, ephemeral: true }); } let amount = randomInteger(1, 20); /* * Legacy Abra bonus. */ if (userId === ABRA_ID) { amount = Math.ceil( amount * ABRA_MULTIPLIER ); } await changeCookies( userId, amount ); await setCooldown( userId, guildId, "collect", COLLECT_COOLDOWN ); return interaction.reply({ content: randomCollectResponse( amount, interaction.user ) }); } /* * /cookies steal */ async function steal(interaction) { const user = interaction.user; const target = interaction.options.getUser( "target", true ); /* * Can't rob yourself. */ if (target.id === user.id) { return interaction.reply({ content: "C'mon, don't embarrass yourself. You can't steal from yourself.", ephemeral: true }); } /* * Don't let people farm bot accounts. */ if (target.bot) { return interaction.reply({ content: "Bots have highly sophisticated cookie security systems.", ephemeral: true }); } const now = getUnixTime(); /* * Check steal cooldown before doing anything expensive. */ const cooldown = await getCooldown( user.id, interaction.guildId, "steal" ); if (cooldown > now) { return interaction.reply({ content: `Lay low for a while. You can steal again .`, ephemeral: true }); } /* * Fetch both balances concurrently. */ const [ yourCookies, theirCookies ] = await Promise.all([ getCookieBalance(user.id), getCookieBalance(target.id) ]); if (yourCookies < 1) { return interaction.reply({ content: "You need at least **1 Cookie** to attempt a robbery.", ephemeral: true }); } if (theirCookies < 1) { return interaction.reply({ content: `Shame on you! ${target} doesn't have any Cookies. D:`, ephemeral: true }); } /* * Chaos mode: * fixed 50% steal chance. */ const success = Math.random() < STEAL_SUCCESS_CHANCE; /* * Successful robbery. */ if (success) { const maxSteal = Math.max( 1, Math.ceil(theirCookies / 4) ); const amount = Math.min( theirCookies, randomInteger( 1, maxSteal ) ); try { await transferCookies( target.id, user.id, amount ); } catch (error) { if (error.message === "INSUFFICIENT_COOKIES") { return interaction.reply({ content: `${target}'s Cookie balance changed before you could finish the robbery.`, ephemeral: true }); } throw error; } const nextSteal = await setCooldown( user.id, interaction.guildId, "steal", STEAL_COOLDOWN ); return interaction.reply({ content: `${randomStealSuccessResponse(amount, target)}\n` + `**Success Chance:** ${formatPercentage(STEAL_SUCCESS_CHANCE)} • ` + `Next robbery ` }); } /* * Failed robbery. * * Lose between 1 and roughly 1/6 of your balance. */ const maxPenalty = Math.max( 1, Math.ceil(yourCookies / 6) ); const penalty = Math.min( yourCookies, randomInteger( 1, maxPenalty ) ); try { await transferCookies( user.id, target.id, penalty ); } catch (error) { if (error.message === "INSUFFICIENT_COOKIES") { return interaction.reply({ content: "Your Cookie balance changed before the robbery could finish.", ephemeral: true }); } throw error; } const nextSteal = await setCooldown( user.id, interaction.guildId, "steal", STEAL_COOLDOWN ); return interaction.reply({ content: `${randomStealFailResponse(penalty, target)}\n` + `**Success Chance:** ${formatPercentage(STEAL_SUCCESS_CHANCE)} • ` + `Next robbery ` }); } /* * /cookies check */ async function check(interaction) { const target = interaction.options.getUser("target") ?? interaction.user; const balance = await getCookieBalance(target.id); const mood = balance === 0 ? "😔" : "🍪"; return interaction.reply({ content: `${target} has **${balance.toLocaleString()} Cookie${balance === 1 ? "" : "s"}**! ${mood}` }); } /* * /cookies give */ async function give(interaction) { const user = interaction.user; const target = interaction.options.getUser( "target", true ); const amount = interaction.options.getInteger( "amount", true ); if (target.id === user.id) { return interaction.reply({ content: "You cannot give Cookies to yourself.", ephemeral: true }); } if (target.bot) { return interaction.reply({ content: "Bots don't need Cookies. Allegedly.", ephemeral: true }); } try { await transferCookies( user.id, target.id, amount ); } catch (error) { if (error.message === "INSUFFICIENT_COOKIES") { return interaction.reply({ content: "We don't run a charity here. You don't have enough Cookies!", ephemeral: true }); } throw error; } return interaction.reply({ content: `${user} shared **${amount.toLocaleString()} Cookie${amount === 1 ? "" : "s"}** with ${target} 🍪` }); } /* * /cookies leaderboard */ async function leaderboard(interaction) { const { cookies } = getCollections(); const results = await cookies .find({ cookies: { $gt: 0 } }) .sort({ cookies: -1 }) .limit(10) .toArray(); const description = results.length ? results .map((record, index) => { const medal = getLeaderboardMedal(index); return ( `${medal} <@${record.userId}> — ` + `**${record.cookies.toLocaleString()}** 🍪` ); }) .join("\n") : "Nobody has collected any Cookies yet."; const embed = new EmbedBuilder() .setTitle("🍪 Cookie Leaderboard") .setDescription(description) .setTimestamp(); if (interaction.guild?.iconURL()) { embed.setThumbnail( interaction.guild.iconURL({ size: 256 }) ); } return interaction.reply({ embeds: [embed] }); } /* * COLLECT RESPONSES */ function randomCollectResponse(amount, user) { const cookies = `**${amount.toLocaleString()} Cookie${amount === 1 ? "" : "s"}**`; const responses = [ `${user}, I made you ${cookies} 🍪`, `${user}, here's your ${cookies} 🍪 I ate the rest, sorry >.<`, `Only ${cookies} 🍪 for you today ${user}... Abra stole the rest :c`, `${user}, would you like a glass of milk with your ${cookies}? 🥛`, `${cookies} 🍪 Careful, ${user}! They're still warm :3`, `${cookies} 🍪 Best batch yet!`, `${user}, these ${cookies} I bequeath unto thine own self upon this day.`, `${user}, your ${cookies} are fresh out of the oven. Try not to burn your tongue!`, `${user}, I bribed the Cookie Monster to spare these ${cookies} just for you.`, `${user}, Santa came early! You got ${cookies}. Milk not included.`, `${user}, your ${cookies} are artisanal, gluten-free, and slightly cursed.`, `A wild baker appeared! They hand ${user} ${cookies} 🍪`, `${user}, Abra said these ${cookies} are "totally not stolen"... 🤔`, `${user}, here's your ${cookies} — watch out, they may be booby-trapped.` ]; return randomItem(responses); } /* * STEAL SUCCESS RESPONSES */ function randomStealSuccessResponse(amount, target) { const cookies = `**${amount.toLocaleString()} Cookie${amount === 1 ? "" : "s"}**`; const responses = [ `You've successfully stolen ${cookies} from ${target}!`, `Ninja mode activated. You snatched ${cookies} from ${target} without leaving a crumb behind.`, `Abra taught you well — ${cookies} from ${target} are now yours!`, `Like a phantom in the night, you took ${cookies} from ${target}.`, `You just pulled the sweetest heist: ${cookies} from ${target}!`, `Mission complete. ${target} is ${cookies} poorer.`, `You distracted ${target} with a dance and swiped ${cookies}.`, `Clean getaway! ${target} won't notice the missing ${cookies} until it's too late.`, `The Cookie Vault has been breached. ${cookies} taken from ${target}.`, `You emerged from the shadows carrying ${cookies} from ${target}.` ]; return randomItem(responses); } /* * STEAL FAILURE RESPONSES */ function randomStealFailResponse(amount, target) { const cookies = `**${amount.toLocaleString()} Cookie${amount === 1 ? "" : "s"}**`; const responses = [ `You were caught red-handed by ${target} and lost ${cookies} as a penalty!`, `Fail! ${target} caught you mid-bite — you dropped ${cookies}.`, `Not your best moment... You lost ${cookies} to ${target} in the struggle.`, `Stealth level: 0. You handed ${cookies} to ${target} as an apology.`, `The Cookie jar was booby-trapped! You gave ${cookies} to ${target} while running away.`, `Ouch. The guard dog barked, and now ${target} has your ${cookies}.`, `You tripped over your own shoelace — ${target} picked up ${cookies}.`, `Caught by the Cookie Police! ${cookies} confiscated and given to ${target}.`, `Your pockets had holes... ${target} found the ${cookies} you dropped.` ]; return randomItem(responses); } /* * GENERIC HELPERS */ function getUnixTime() { return Math.floor( Date.now() / 1000 ); } function randomInteger(min, max) { return Math.floor( Math.random() * (max - min + 1) ) + min; } function randomItem(items) { return items[ Math.floor( Math.random() * items.length ) ]; } function formatPercentage(value) { return `${Math.round(value * 100)}%`; } function getLeaderboardMedal(index) { switch (index) { case 0: return "🥇"; case 1: return "🥈"; case 2: return "🥉"; default: return `**${index + 1}.**`; } }