const { SlashCommandBuilder, EmbedBuilder, MessageFlags } = require("discord.js"); const { getCollections, getCookieBalance, changeCookies, transferCookies, claimCooldown, randomInteger, randomFloat, randomItem, formatPercentage } = require( "../../utils/cookieEconomy" ); const { trackProfileEvent } = require( "../../utils/profileSystem" ); /* * ============================================================ * CONFIG * ============================================================ */ const COLLECT_COOLDOWN = 60 * 60; // 1 hour const STEAL_COOLDOWN = 15 * 60; // 15 minutes const ABRA_ID = "378135175059603457"; const ABRA_MULTIPLIER = 1.5; /* * ============================================================ * COMMAND * ============================================================ */ 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.", flags: MessageFlags.Ephemeral }); } } }; /* * ============================================================ * /cookies collect * ============================================================ */ async function collect( interaction ) { const userId = interaction.user.id; /* * Shared Mongo-backed cooldown. * * This is the same key the Crumb Saucer * Cookie Bakery should use. */ const cooldown = await claimCooldown( userId, interaction.guildId, "collect", COLLECT_COOLDOWN ); if ( !cooldown.claimed ) { return interaction.reply({ content: `The batch is still cooking. Come back 🍪`, flags: MessageFlags.Ephemeral }); } /* * Generate batch. */ let amount = randomInteger( 1, 20 ); /* * Legacy Abra bonus. */ if ( userId === ABRA_ID ) { amount = Math.ceil( amount * ABRA_MULTIPLIER ); } const balance = await changeCookies( userId, amount ); /* * Lifetime Cookie gain + achievements. */ await trackProfileEvent( interaction, userId, { type: "cookie_gain", amount } ); const embed = new EmbedBuilder() .setTitle( "🍪 Fresh Cookies!" ) .setDescription( randomCollectResponse( amount, interaction.user ) ) .addFields( { name: "Collected", value: `**+${amount.toLocaleString()} 🍪**`, inline: true }, { name: "Your Vault", value: `**${balance.toLocaleString()} 🍪**`, inline: true }, { name: "Next Batch", value: ``, inline: false } ) .setThumbnail( interaction.user.displayAvatarURL({ size: 256 }) ) .setTimestamp(); return interaction.reply({ embeds: [ embed ] }); } /* * ============================================================ * /cookies steal * ============================================================ */ async function steal( interaction ) { const user = interaction.user; const target = interaction.options.getUser( "target", true ); /* * ======================================================== * VALIDATION * ======================================================== */ if ( target.id === user.id ) { return interaction.reply({ content: "C'mon, don't embarrass yourself. You can't steal from yourself.", flags: MessageFlags.Ephemeral }); } if ( target.bot ) { return interaction.reply({ content: "Bots have highly sophisticated Cookie security systems.", flags: MessageFlags.Ephemeral }); } /* * ======================================================== * BALANCES * ======================================================== */ 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.", flags: MessageFlags.Ephemeral }); } if ( theirCookies < 1 ) { return interaction.reply({ content: `Shame on you! ${target} doesn't have any Cookies. D:`, flags: MessageFlags.Ephemeral }); } /* * ======================================================== * COOLDOWN * ======================================================== * * Claim before resolving the robbery. * * This prevents double clicks / concurrent steals. */ const cooldown = await claimCooldown( user.id, interaction.guildId, "steal", STEAL_COOLDOWN ); if ( !cooldown.claimed ) { return interaction.reply({ content: `Lay low for a while. You can steal again .`, flags: MessageFlags.Ephemeral }); } /* * ======================================================== * DYNAMIC SUCCESS CHANCE * ======================================================== */ const successChance = getStealChance( yourCookies, theirCookies ); const success = Math.random() < successChance; /* * ======================================================== * SUCCESS * ======================================================== */ if (success) { const { amount } = getStealAmount( theirCookies ); 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.`, flags: MessageFlags.Ephemeral }); } throw error; } /* * Thief stats. */ await trackProfileEvent( interaction, user.id, { type: "steal_success", amount, targetUserId: target.id } ); await trackProfileEvent( interaction, user.id, { type: "cookie_gain", amount } ); /* * Victim stats. */ await trackProfileEvent( interaction, target.id, { type: "stolen_from_me", amount, thiefUserId: user.id } ); await trackProfileEvent( interaction, target.id, { type: "cookie_loss", amount } ); const [ yourBalanceAfter, targetBalanceAfter ] = await Promise.all([ getCookieBalance( user.id ), getCookieBalance( target.id ) ]); const actualPercentage = ( amount / theirCookies ) * 100; const embed = new EmbedBuilder() .setTitle( "🥷 Robbery Successful!" ) .setDescription( randomStealSuccessResponse( amount, target ) ) .addFields( { name: "Cookies Stolen", value: `**${amount.toLocaleString()} 🍪**`, inline: true }, { name: "Vault Taken", value: `**${actualPercentage.toFixed(1)}%**`, inline: true }, { name: "Success Chance", value: `**${formatPercentage(successChance)}**`, inline: true }, { name: "Your Vault", value: `**${yourBalanceAfter.toLocaleString()} 🍪**`, inline: true }, { name: `${target.username}'s Vault`, value: `**${targetBalanceAfter.toLocaleString()} 🍪**`, inline: true }, { name: "Next Robbery", value: ``, inline: false } ) .setThumbnail( target.displayAvatarURL({ size: 256 }) ) .setTimestamp(); return interaction.reply({ embeds: [ embed ] }); } /* * ======================================================== * FAILURE * ======================================================== * * Penalty remains percentage based. */ const penaltyPercentage = randomFloat( 0.01, 0.08 ); const penalty = Math.max( 1, Math.min( yourCookies, Math.floor( yourCookies * penaltyPercentage ) ) ); 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.", flags: MessageFlags.Ephemeral }); } throw error; } /* * Profile tracking. */ await trackProfileEvent( interaction, user.id, { type: "cookie_loss", amount: penalty } ); await trackProfileEvent( interaction, target.id, { type: "cookie_gain", amount: penalty } ); const [ yourBalanceAfter, targetBalanceAfter ] = await Promise.all([ getCookieBalance( user.id ), getCookieBalance( target.id ) ]); const actualPenaltyPercentage = ( penalty / yourCookies ) * 100; const embed = new EmbedBuilder() .setTitle( "🚨 Robbery Failed!" ) .setDescription( randomStealFailResponse( penalty, target ) ) .addFields( { name: "Fine", value: `**${penalty.toLocaleString()} 🍪**`, inline: true }, { name: "Vault Lost", value: `**${actualPenaltyPercentage.toFixed(1)}%**`, inline: true }, { name: "Success Chance", value: `**${formatPercentage(successChance)}**`, inline: true }, { name: "Your Vault", value: `**${yourBalanceAfter.toLocaleString()} 🍪**`, inline: true }, { name: `${target.username}'s Vault`, value: `**${targetBalanceAfter.toLocaleString()} 🍪**`, inline: true }, { name: "Next Robbery", value: ``, inline: false } ) .setThumbnail( target.displayAvatarURL({ size: 256 }) ) .setTimestamp(); return interaction.reply({ embeds: [ embed ] }); } /* * ============================================================ * /cookies check * ============================================================ */ async function check( interaction ) { const target = interaction.options.getUser( "target" ) ?? interaction.user; const balance = await getCookieBalance( target.id ); const embed = new EmbedBuilder() .setTitle( "🍪 Cookie Vault" ) .setDescription( `${target} has **${balance.toLocaleString()} Cookie${balance === 1 ? "" : "s"}**.` ) .addFields({ name: "Vault Balance", value: `**${balance.toLocaleString()} 🍪**` }) .setThumbnail( target.displayAvatarURL({ size: 256 }) ) .setTimestamp(); return interaction.reply({ embeds: [ embed ] }); } /* * ============================================================ * /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.", flags: MessageFlags.Ephemeral }); } if ( target.bot ) { return interaction.reply({ content: "Bots don't need Cookies. Allegedly.", flags: MessageFlags.Ephemeral }); } 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!", flags: MessageFlags.Ephemeral }); } throw error; } /* * Sender lifetime giving. */ await trackProfileEvent( interaction, user.id, { type: "cookies_given", amount } ); /* * Recipient lifetime gain. */ await trackProfileEvent( interaction, target.id, { type: "cookie_gain", amount } ); const [ senderBalance, recipientBalance ] = await Promise.all([ getCookieBalance( user.id ), getCookieBalance( target.id ) ]); const embed = new EmbedBuilder() .setTitle( "🍪 Cookies Shared!" ) .setDescription( `${user} shared **${amount.toLocaleString()} Cookie${amount === 1 ? "" : "s"}** with ${target}.` ) .addFields( { name: "Transferred", value: `**${amount.toLocaleString()} 🍪**`, inline: true }, { name: "Your Vault", value: `**${senderBalance.toLocaleString()} 🍪**`, inline: true }, { name: `${target.username}'s Vault`, value: `**${recipientBalance.toLocaleString()} 🍪**`, inline: true } ) .setThumbnail( target.displayAvatarURL({ size: 256 }) ) .setTimestamp(); return interaction.reply({ embeds: [ embed ] }); } /* * ============================================================ * /cookies leaderboard * ============================================================ */ async function leaderboard( interaction ) { const { cookies } = getCollections(); /* * Keep ShaiBot's central vault off * the player leaderboard. */ const results = await cookies .find({ cookies: { $gt: 0 }, userId: { $ne: interaction.client.user.id } }) .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 ); } /* * ============================================================ * STEAL CHANCE * ============================================================ */ function getStealChance( attackerCookies, targetCookies ) { const MIN_CHANCE = 0.15; const MAX_CHANCE = 0.70; const BASE_CHANCE = 0.50; /* * Wealthier targets become harder to rob. */ const targetPenalty = Math.log10( targetCookies + 1 ) * 0.06; /* * Attacker / victim wealth ratio. */ const wealthRatio = attackerCookies / Math.max( targetCookies, 1 ); /* * Catch-up mechanic. * * Poorer attackers receive a small bonus. */ const povertyBonus = wealthRatio < 1 ? ( 1 - wealthRatio ) * 0.18 : 0; /* * Rich players bullying poorer targets * receive a success penalty. */ const bullyPenalty = wealthRatio > 1 ? Math.min( Math.log10( wealthRatio + 1 ) * 0.12, 0.20 ) : 0; const chance = BASE_CHANCE - targetPenalty + povertyBonus - bullyPenalty; return Math.max( MIN_CHANCE, Math.min( MAX_CHANCE, chance ) ); } /* * ============================================================ * STEAL AMOUNT * ============================================================ */ function getStealAmount( targetCookies ) { const MAX_STEAL_AMOUNT = 5000; const percentage = getStealPercentage(); const calculatedAmount = Math.floor( targetCookies * percentage ); const amount = Math.max( 1, Math.min( calculatedAmount, targetCookies, MAX_STEAL_AMOUNT ) ); return { amount, percentage }; } /* * ============================================================ * STEAL PERCENTAGE * ============================================================ */ function getStealPercentage() { const roll = Math.random(); /* * 50%: * 1–3% */ if ( roll < 0.50 ) { return randomFloat( 0.01, 0.03 ); } /* * 30%: * 3–5% */ if ( roll < 0.80 ) { return randomFloat( 0.03, 0.05 ); } /* * 15%: * 5–8% */ if ( roll < 0.95 ) { return randomFloat( 0.05, 0.08 ); } /* * 5% jackpot: * 8–10% */ return randomFloat( 0.08, 0.10 ); } /* * ============================================================ * LEADERBOARD MEDALS * ============================================================ */ function getLeaderboardMedal( index ) { switch ( index ) { case 0: return "🥇"; case 1: return "🥈"; case 2: return "🥉"; default: return `**${index + 1}.**`; } } /* * ============================================================ * EXPORTS FOR CRUMB SAUCER * ============================================================ * * This lets the Saucer reuse the same collect implementation * later if we decide to route its bakery directly through here. */ module.exports.collect = collect;