const { SlashCommandBuilder, EmbedBuilder, MessageFlags } = require("discord.js"); const { getCollections, getCookieBalance, transferCookies, claimCooldown, randomFloat, randomItem, formatPercentage } = require("../../utils/cookieEconomy"); const { trackProfileEvent } = require("../../utils/profileSystem"); const { collectCookies } = require("../../utils/crumbCollect"); 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; const result = await collectCookies(interaction, userId); if (!result.collected) { return interaction.reply({ content: `The batch is still cooking. Come back 🍪`, flags: MessageFlags.Ephemeral }); } const embed = new EmbedBuilder() .setTitle("🍪 Fresh Cookies!") .setDescription( randomCollectResponse( result.amount, interaction.user ) ) .addFields( { name: "Collected", value: `**+${result.amount.toLocaleString()} 🍪**`, inline: true }, { name: "Your Vault", value: `**${result.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); /* * Command-specific balancing. * * Base success chance: * 100x richer attacker -> 30% * 10x richer attacker -> 42.5% * equal wealth -> 55% * 10x poorer attacker -> 67.5% * 100x poorer attacker -> 80% */ const cooldownSeconds = 15 * 60; const minSuccessChance = 0.30; const maxSuccessChance = 0.80; const baseSuccessChance = 0.55; const wealthChanceScale = 0.125; const heatWindowMs = 60 * 60 * 1000; const heatPerAttempt = 0.075; const hourlyStealablePercentage = 0.15; const maxStealAmount = 25000; const baseFailurePenaltyMin = 0.02; const baseFailurePenaltyMax = 0.10; const minPenaltyMultiplier = 0.75; const maxPenaltyMultiplier = 1.50; const minRewardMultiplier = 0.75; const maxRewardMultiplier = 1.25; const stealTiers = [ { threshold: 0.50, min: 0.01, max: 0.03 }, { threshold: 0.80, min: 0.03, max: 0.05 }, { threshold: 0.95, min: 0.05, max: 0.08 }, { threshold: 1.00, min: 0.08, max: 0.10 } ]; 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 }); } 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 }); } const { cookies, statsProfile } = getCookieCommandCollections({ requireStatsProfile: true }); const now = new Date(); const stealWindowCutoff = new Date( now.getTime() - heatWindowMs ); /* * Check an active exhausted window before consuming the attacker's * cooldown. Expired windows are allowed to continue and will be reopened * only after the attacker passes their own cooldown check. */ const existingTargetCookieRecord = await cookies.findOne( { userId: target.id }, { projection: { stealable: 1 } } ); const existingStealable = existingTargetCookieRecord?.stealable; const existingWindowStartedAt = existingStealable?.windowStartedAt ? new Date( existingStealable.windowStartedAt ) : null; const existingWindowActive = existingWindowStartedAt && existingWindowStartedAt > stealWindowCutoff; if ( existingWindowActive && existingStealable.amount <= 0 ) { const availableAt = Math.floor( ( existingWindowStartedAt.getTime() + heatWindowMs ) / 1000 ); return interaction.reply({ content: `${target}'s Cookie vault is no longer exposed. Try again .`, flags: MessageFlags.Ephemeral }); } const cooldown = await claimCooldown( user.id, interaction.guildId, "steal", cooldownSeconds ); if (!cooldown.claimed) { return interaction.reply({ content: `Lay low for a while. You can steal again .`, flags: MessageFlags.Ephemeral }); } /* * Hourly victim-loss budget. * * This is a fixed one-hour window. The timestamp does not move when * Cookies are stolen. Once the window expires, the first valid robbery * attempt opens a fresh window and exposes 15% of the victim's current * balance. */ const initialStealableAmount = Math.max( 1, Math.floor( theirCookies * hourlyStealablePercentage ) ); const stealableWindowUpdate = await cookies.updateOne( { userId: target.id, $or: [ { "stealable.windowStartedAt": { $exists: false } }, { "stealable.windowStartedAt": { $lte: stealWindowCutoff } } ] }, { $set: { stealable: { amount: initialStealableAmount, windowStartedAt: now } } } ); const openedStealableWindow = stealableWindowUpdate.modifiedCount === 1; const targetCookieRecord = await cookies.findOne( { userId: target.id }, { projection: { stealable: 1 } } ); const stealable = targetCookieRecord?.stealable; if (!stealable) { throw new Error( `Missing stealable state for Cookie user ${target.id}.` ); } if (stealable.amount <= 0) { const availableAt = Math.floor( ( new Date( stealable.windowStartedAt ).getTime() + heatWindowMs ) / 1000 ); return interaction.reply({ content: `${target}'s Cookie vault is no longer exposed. Try again .`, flags: MessageFlags.Ephemeral }); } const vaultBreachText = openedStealableWindow ? ( `🔓 **Vault Breached!**\n` + `You've breached the security of ${target}'s Vault and exposed some of their assets. ` + `These Cookies can now be stolen until the Vault is repaired!` ) : null; /* * Rolling heat protection. * * Every valid attempt against this target contributes 7.5 percentage * points of heat and fades linearly to zero over one hour. Attempts by * all users count toward the same target heat. */ await statsProfile.updateOne( { userId: target.id }, { $pull: { "stealProtection.attempts": { timestamp: { $lte: stealWindowCutoff } } } } ); /* * Append the current attempt atomically and return the profile state from * immediately before it was appended. This lets simultaneous attackers * see each other's newly-created heat as MongoDB serializes the updates, * while the current attempt still never penalizes itself. */ const heatUpdateResult = await statsProfile.findOneAndUpdate( { userId: target.id }, { $push: { "stealProtection.attempts": { userId: user.id, timestamp: now } } }, { upsert: true, returnDocument: "before" } ); const targetProfile = heatUpdateResult?.value ?? heatUpdateResult; const activeAttempts = targetProfile?.stealProtection?.attempts ?? []; const heat = activeAttempts.reduce( (total, attempt) => { const timestamp = new Date( attempt.timestamp ).getTime(); const age = now.getTime() - timestamp; if ( !Number.isFinite(age) || age < 0 || age >= heatWindowMs ) { return total; } const remainingWeight = 1 - age / heatWindowMs; return total + heatPerAttempt * remainingWeight; }, 0 ); const wealthDifference = Math.log10( (theirCookies + 1) / (yourCookies + 1) ); const unheatedSuccessChance = Math.max( minSuccessChance, Math.min( maxSuccessChance, baseSuccessChance + wealthDifference * wealthChanceScale ) ); const successChance = Math.max( minSuccessChance, unheatedSuccessChance - heat ); const success = Math.random() < successChance; if (success) { /* * Reward scales inversely with final success chance: * 80% chance -> 0.75x loot range * 55% chance -> 1.00x loot range * 30% chance -> 1.25x loot range */ const chancePosition = (successChance - minSuccessChance) / (maxSuccessChance - minSuccessChance); const rewardMultiplier = maxRewardMultiplier - chancePosition * (maxRewardMultiplier - minRewardMultiplier); const lootRoll = Math.random(); const lootTier = stealTiers.find( tier => lootRoll < tier.threshold ) ?? stealTiers[stealTiers.length - 1]; const stealPercentage = randomFloat( lootTier.min * rewardMultiplier, lootTier.max * rewardMultiplier ); const requestedAmount = Math.max( 1, Math.min( Math.floor( theirCookies * stealPercentage ), theirCookies, maxStealAmount, stealable.amount ) ); /* * Reserve from the hourly stealable budget atomically before the * transfer. Concurrent robberies cannot reserve more than remains. */ const reservation = await cookies.updateOne( { userId: target.id, "stealable.windowStartedAt": new Date( stealable.windowStartedAt ), "stealable.amount": { $gte: requestedAmount } }, { $inc: { "stealable.amount": -requestedAmount } } ); if (reservation.modifiedCount !== 1) { return interaction.reply({ content: `${target}'s robbery protection changed before you could finish the heist. No Cookies were taken.`, flags: MessageFlags.Ephemeral }); } try { await transferCookies( target.id, user.id, requestedAmount ); } catch (error) { /* * Refund the reservation when the balance transfer cannot be * completed. The fixed window timestamp remains unchanged. */ await cookies.updateOne( { userId: target.id, "stealable.windowStartedAt": new Date( stealable.windowStartedAt ) }, { $inc: { "stealable.amount": requestedAmount } } ); 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; } const amount = requestedAmount; await trackProfileEvent( interaction, user.id, { type: "steal_success", source: "steal", amount, targetUserId: target.id } ); await trackProfileEvent( interaction, user.id, { type: "cookie_gain", source: "steal", amount, targetUserId: target.id } ); await trackProfileEvent( interaction, target.id, { type: "stolen_from_me", source: "steal", amount, thiefUserId: user.id } ); await trackProfileEvent( interaction, target.id, { type: "cookie_loss", source: "steal", amount, thiefUserId: user.id } ); const [ yourBalanceAfter, targetBalanceAfter, updatedTargetCookieRecord ] = await Promise.all([ getCookieBalance(user.id), getCookieBalance(target.id), cookies.findOne( { userId: target.id }, { projection: { stealable: 1 } } ) ]); const actualPercentage = amount / theirCookies * 100; const remainingStealable = Math.max( 0, updatedTargetCookieRecord?.stealable?.amount ?? 0 ); const embed = new EmbedBuilder() .setTitle("🥷 Robbery Successful!") .setDescription( [ vaultBreachText, randomStealSuccessResponse( amount, target ) ] .filter(Boolean) .join("\n\n") ) .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: "Vault Exposed", value: `**${remainingStealable.toLocaleString()} 🍪**`, inline: true }, { name: "Next Robbery", value: ``, inline: false } ) .setThumbnail( target.displayAvatarURL({ size: 256 }) ) .setTimestamp(); return interaction.reply({ embeds: [embed] }); } /* * Failure penalty scales with final success chance: * 30% chance -> 1.5% to 7.5% * 55% chance -> 2.25% to 11.25% * 80% chance -> 3% to 15% */ const chancePosition = (successChance - minSuccessChance) / (maxSuccessChance - minSuccessChance); const penaltyMultiplier = minPenaltyMultiplier + chancePosition * (maxPenaltyMultiplier - minPenaltyMultiplier); const penaltyPercentage = randomFloat( baseFailurePenaltyMin * penaltyMultiplier, baseFailurePenaltyMax * penaltyMultiplier ); 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; } await trackProfileEvent( interaction, user.id, { type: "cookie_loss", source: "steal_penalty", amount: penalty, targetUserId: target.id } ); await trackProfileEvent( interaction, target.id, { type: "cookie_gain", source: "steal_penalty", amount: penalty, thiefUserId: user.id } ); const [ yourBalanceAfter, targetBalanceAfter, updatedTargetCookieRecord ] = await Promise.all([ getCookieBalance(user.id), getCookieBalance(target.id), cookies.findOne( { userId: target.id }, { projection: { stealable: 1 } } ) ]); const actualPenaltyPercentage = penalty / yourCookies * 100; const remainingStealable = Math.max( 0, updatedTargetCookieRecord?.stealable?.amount ?? 0 ); const embed = new EmbedBuilder() .setTitle("🚨 Robbery Failed!") .setDescription( [ vaultBreachText, randomStealFailResponse( penalty, target ) ] .filter(Boolean) .join("\n\n") ) .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: "Vault Exposed", value: `**${remainingStealable.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; } await trackProfileEvent( interaction, user.id, { type: "cookies_given", source: "give", amount, targetUserId: target.id } ); await trackProfileEvent( interaction, target.id, { type: "cookie_gain", source: "give", amount, senderUserId: user.id } ); 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 } = getCookieCommandCollections(); 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] }); } /* * ============================================================ * COLLECTION ACCESS * ============================================================ * * getCollections() already exposes the Cookie collection in the existing * application. The fallbacks below allow either camelCase or raw Mongo * collection names for the two collections used by this command. */ function getCookieCommandCollections({ requireStatsProfile = false } = {}) { const collections = getCollections(); const database = collections.db ?? collections.database; const cookies = collections.cookies ?? collections.itemsCookies ?? collections.items_cookies ?? database?.collection?.("items_cookies"); const statsProfile = collections.statsProfile ?? collections.statsProfiles ?? collections.stats_profile ?? database?.collection?.("stats_profile"); if (!cookies) { throw new Error( "getCollections() must expose the items_cookies collection as cookies/itemsCookies/items_cookies, or expose a db handle." ); } if (requireStatsProfile && !statsProfile) { throw new Error( "getCollections() must expose the stats_profile collection as statsProfile/statsProfiles/stats_profile, or expose a db handle." ); } return { cookies, statsProfile }; } /* * ============================================================ * 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); } /* * ============================================================ * LEADERBOARD MEDALS * ============================================================ */ function getLeaderboardMedal(index) { switch (index) { case 0: return "🥇"; case 1: return "🥈"; case 2: return "🥉"; default: return `**${index + 1}.**`; } }