diff --git a/commands/applications/cookies.js b/commands/applications/cookies.js index 0ecda53..4dc093f 100644 --- a/commands/applications/cookies.js +++ b/commands/applications/cookies.js @@ -7,22 +7,20 @@ const { const { getCollections, getCookieBalance, - changeCookies, transferCookies, claimCooldown, - randomInteger, randomFloat, randomItem, formatPercentage -} = require( - "../../utils/cookieEconomy" -); +} = require("../../utils/cookieEconomy"); const { trackProfileEvent -} = require( - "../../utils/profileSystem" -); +} = require("../../utils/profileSystem"); + +const { + collectCookies +} = require("../../utils/crumbCollect"); /* @@ -31,20 +29,7 @@ const { * ============================================================ */ -const COLLECT_COOLDOWN = - 60 * 60; // 1 hour - - -const STEAL_COOLDOWN = - 15 * 60; // 15 minutes - - -const ABRA_ID = - "378135175059603457"; - - -const ABRA_MULTIPLIER = - 1.5; +const STEAL_COOLDOWN = 15 * 60; // 15 minutes /* @@ -56,21 +41,13 @@ const ABRA_MULTIPLIER = module.exports = { data: new SlashCommandBuilder() - .setName( - "cookies" - ) - - .setDescription( - "The ShaiBot cookie economy." - ) + .setName("cookies") + .setDescription("The ShaiBot cookie economy.") .addSubcommand( subcommand => subcommand - .setName( - "collect" - ) - + .setName("collect") .setDescription( "Collect a fresh batch of cookies." ) @@ -79,49 +56,32 @@ module.exports = { .addSubcommand( subcommand => subcommand - .setName( - "steal" - ) - + .setName("steal") .setDescription( "Attempt to steal another user's cookies." ) - .addUserOption( option => option - .setName( - "target" - ) - + .setName("target") .setDescription( "The unfortunate victim." ) - - .setRequired( - true - ) + .setRequired(true) ) ) .addSubcommand( subcommand => subcommand - .setName( - "check" - ) - + .setName("check") .setDescription( "Check a cookie balance." ) - .addUserOption( option => option - .setName( - "target" - ) - + .setName("target") .setDescription( "The user whose cookies you want to check." ) @@ -131,111 +91,63 @@ module.exports = { .addSubcommand( subcommand => subcommand - .setName( - "give" - ) - + .setName("give") .setDescription( "Give some of your cookies to another user." ) - .addUserOption( option => option - .setName( - "target" - ) - + .setName("target") .setDescription( "The recipient." ) - - .setRequired( - true - ) + .setRequired(true) ) - .addIntegerOption( option => option - .setName( - "amount" - ) - + .setName("amount") .setDescription( "How many cookies to give." ) - - .setRequired( - true - ) - - .setMinValue( - 1 - ) + .setRequired(true) + .setMinValue(1) ) ) .addSubcommand( subcommand => subcommand - .setName( - "leaderboard" - ) - + .setName("leaderboard") .setDescription( "Show the richest cookie collectors." ) ), - - async execute( - interaction - ) { + async execute(interaction) { switch ( interaction.options.getSubcommand() ) { case "collect": - - return collect( - interaction - ); - + return collect(interaction); case "steal": - - return steal( - interaction - ); - + return steal(interaction); case "check": - - return check( - interaction - ); - + return check(interaction); case "give": - - return give( - interaction - ); - + return give(interaction); case "leaderboard": - - return leaderboard( - interaction - ); - + return leaderboard(interaction); default: - return interaction.reply({ content: "Unknown subcommand.", - flags: MessageFlags.Ephemeral }); @@ -250,36 +162,41 @@ module.exports = { * ============================================================ */ -async function collect( - interaction -) { +async function collect(interaction) { const userId = interaction.user.id; - /* - * Shared Mongo-backed cooldown. + * Shared collection logic from crumbCollect.js. * - * This is the same key the Crumb Saucer - * Cookie Bakery should use. + * This now owns: + * + * - Mongo cooldown + * - collect amount + * - balance mutation + * - profile tracking */ - const cooldown = - await claimCooldown( - userId, - interaction.guildId, - "collect", - COLLECT_COOLDOWN + const result = + await collectCookies( + interaction, + userId ); - if ( - !cooldown.claimed - ) { + /* + * ======================================================== + * COOLDOWN + * ======================================================== + * + * Keep the existing /cookies behavior: + * cooldown errors are private/ephemeral. + */ + + if (!result.collected) { return interaction.reply({ content: - `The batch is still cooking. Come back 🍪`, - + `The batch is still cooking. Come back 🍪`, flags: MessageFlags.Ephemeral }); @@ -287,117 +204,61 @@ async function collect( /* - * Generate batch. + * ======================================================== + * SUCCESS EMBED + * ======================================================== + * + * Preserve the existing cookies.js embed and random + * response style. */ - 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, + result.amount, interaction.user ) ) - .addFields( { name: "Collected", - value: - `**+${amount.toLocaleString()} 🍪**`, - + `**+${result.amount.toLocaleString()} 🍪**`, inline: true }, - { name: "Your Vault", - value: - `**${balance.toLocaleString()} 🍪**`, - + `**${result.balance.toLocaleString()} 🍪**`, inline: true }, - { name: "Next Batch", - value: - ``, - + ``, inline: false } ) - .setThumbnail( interaction.user.displayAvatarURL({ - size: - 256 + size: 256 }) ) - .setTimestamp(); return interaction.reply({ - embeds: [ - embed - ] + embeds: [embed] }); } @@ -408,13 +269,10 @@ async function collect( * ============================================================ */ -async function steal( - interaction -) { +async function steal(interaction) { const user = interaction.user; - const target = interaction.options.getUser( "target", @@ -435,7 +293,6 @@ async function steal( return interaction.reply({ content: "C'mon, don't embarrass yourself. You can't steal from yourself.", - flags: MessageFlags.Ephemeral }); @@ -448,7 +305,6 @@ async function steal( return interaction.reply({ content: "Bots have highly sophisticated Cookie security systems.", - flags: MessageFlags.Ephemeral }); @@ -469,7 +325,6 @@ async function steal( getCookieBalance( user.id ), - getCookieBalance( target.id ) @@ -483,7 +338,6 @@ async function steal( return interaction.reply({ content: "You need at least **1 Cookie** to attempt a robbery.", - flags: MessageFlags.Ephemeral }); @@ -497,7 +351,6 @@ async function steal( return interaction.reply({ content: `Shame on you! ${target} doesn't have any Cookies. D:`, - flags: MessageFlags.Ephemeral }); @@ -508,10 +361,6 @@ async function steal( * ======================================================== * COOLDOWN * ======================================================== - * - * Claim before resolving the robbery. - * - * This prevents double clicks / concurrent steals. */ const cooldown = @@ -529,7 +378,6 @@ async function steal( return interaction.reply({ content: `Lay low for a while. You can steal again .`, - flags: MessageFlags.Ephemeral }); @@ -575,8 +423,9 @@ async function steal( user.id, amount ); + } - } catch (error) { + catch (error) { if ( error.message === "INSUFFICIENT_COOKIES" @@ -584,7 +433,6 @@ async function steal( return interaction.reply({ content: `${target}'s Cookie balance changed before you could finish the robbery.`, - flags: MessageFlags.Ephemeral }); @@ -605,9 +453,7 @@ async function steal( { type: "steal_success", - amount, - targetUserId: target.id } @@ -620,7 +466,6 @@ async function steal( { type: "cookie_gain", - amount } ); @@ -636,9 +481,7 @@ async function steal( { type: "stolen_from_me", - amount, - thiefUserId: user.id } @@ -651,7 +494,6 @@ async function steal( { type: "cookie_loss", - amount } ); @@ -665,7 +507,6 @@ async function steal( getCookieBalance( user.id ), - getCookieBalance( target.id ) @@ -686,96 +527,72 @@ async function steal( .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 + size: 256 }) ) - .setTimestamp(); return interaction.reply({ - embeds: [ - embed - ] + embeds: [embed] }); } @@ -784,8 +601,6 @@ async function steal( * ======================================================== * FAILURE * ======================================================== - * - * Penalty remains percentage based. */ const penaltyPercentage = @@ -798,10 +613,8 @@ async function steal( const penalty = Math.max( 1, - Math.min( yourCookies, - Math.floor( yourCookies * penaltyPercentage @@ -816,8 +629,9 @@ async function steal( target.id, penalty ); + } - } catch (error) { + catch (error) { if ( error.message === "INSUFFICIENT_COOKIES" @@ -825,7 +639,6 @@ async function steal( return interaction.reply({ content: "Your Cookie balance changed before the robbery could finish.", - flags: MessageFlags.Ephemeral }); @@ -846,7 +659,6 @@ async function steal( { type: "cookie_loss", - amount: penalty } @@ -859,7 +671,6 @@ async function steal( { type: "cookie_gain", - amount: penalty } @@ -874,7 +685,6 @@ async function steal( getCookieBalance( user.id ), - getCookieBalance( target.id ) @@ -895,96 +705,72 @@ async function steal( .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 + size: 256 }) ) - .setTimestamp(); return interaction.reply({ - embeds: [ - embed - ] + embeds: [embed] }); } @@ -995,9 +781,7 @@ async function steal( * ============================================================ */ -async function check( - interaction -) { +async function check(interaction) { const target = interaction.options.getUser( "target" @@ -1017,33 +801,25 @@ async function check( .setTitle( "🍪 Cookie Vault" ) - .setDescription( `${target} has **${balance.toLocaleString()} Cookie${balance === 1 ? "" : "s"}**.` ) - .addFields({ name: "Vault Balance", - value: `**${balance.toLocaleString()} 🍪**` }) - .setThumbnail( target.displayAvatarURL({ - size: - 256 + size: 256 }) ) - .setTimestamp(); return interaction.reply({ - embeds: [ - embed - ] + embeds: [embed] }); } @@ -1054,20 +830,16 @@ async function check( * ============================================================ */ -async function give( - interaction -) { +async function give(interaction) { const user = interaction.user; - const target = interaction.options.getUser( "target", true ); - const amount = interaction.options.getInteger( "amount", @@ -1082,7 +854,6 @@ async function give( return interaction.reply({ content: "You cannot give Cookies to yourself.", - flags: MessageFlags.Ephemeral }); @@ -1095,7 +866,6 @@ async function give( return interaction.reply({ content: "Bots don't need Cookies. Allegedly.", - flags: MessageFlags.Ephemeral }); @@ -1108,8 +878,9 @@ async function give( target.id, amount ); + } - } catch (error) { + catch (error) { if ( error.message === "INSUFFICIENT_COOKIES" @@ -1117,7 +888,6 @@ async function give( return interaction.reply({ content: "We don't run a charity here. You don't have enough Cookies!", - flags: MessageFlags.Ephemeral }); @@ -1138,7 +908,6 @@ async function give( { type: "cookies_given", - amount } ); @@ -1154,7 +923,6 @@ async function give( { type: "cookie_gain", - amount } ); @@ -1168,7 +936,6 @@ async function give( getCookieBalance( user.id ), - getCookieBalance( target.id ) @@ -1180,60 +947,45 @@ async function give( .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 + size: 256 }) ) - .setTimestamp(); return interaction.reply({ - embeds: [ - embed - ] + embeds: [embed] }); } @@ -1244,26 +996,18 @@ async function give( * ============================================================ */ -async function leaderboard( - interaction -) { +async function leaderboard(interaction) { const { cookies } = getCollections(); - /* - * Keep ShaiBot's central vault off - * the player leaderboard. - */ - const results = await cookies .find({ cookies: { - $gt: - 0 + $gt: 0 }, userId: { @@ -1271,16 +1015,10 @@ async function leaderboard( interaction.client.user.id } }) - .sort({ - cookies: - -1 + cookies: -1 }) - - .limit( - 10 - ) - + .limit(10) .toArray(); @@ -1297,18 +1035,13 @@ async function leaderboard( index ); - return ( `${medal} <@${record.userId}> — ` + `**${record.cookies.toLocaleString()}** 🍪` ); } ) - - .join( - "\n" - ) - + .join("\n") : "Nobody has collected any Cookies yet."; @@ -1317,11 +1050,9 @@ async function leaderboard( .setTitle( "🍪 Cookie Leaderboard" ) - .setDescription( description ) - .setTimestamp(); @@ -1331,17 +1062,14 @@ async function leaderboard( ) { embed.setThumbnail( interaction.guild.iconURL({ - size: - 256 + size: 256 }) ); } return interaction.reply({ - embeds: [ - embed - ] + embeds: [embed] }); } @@ -1461,22 +1189,11 @@ function getStealChance( attackerCookies, targetCookies ) { - const MIN_CHANCE = - 0.15; + const MIN_CHANCE = 0.15; + const MAX_CHANCE = 0.70; + const BASE_CHANCE = 0.50; - const MAX_CHANCE = - 0.70; - - - const BASE_CHANCE = - 0.50; - - - /* - * Wealthier targets become harder to rob. - */ - const targetPenalty = Math.log10( targetCookies + @@ -1486,10 +1203,6 @@ function getStealChance( 0.06; - /* - * Attacker / victim wealth ratio. - */ - const wealthRatio = attackerCookies / Math.max( @@ -1498,12 +1211,6 @@ function getStealChance( ); - /* - * Catch-up mechanic. - * - * Poorer attackers receive a small bonus. - */ - const povertyBonus = wealthRatio < 1 @@ -1513,15 +1220,9 @@ function getStealChance( ) * 0.18 - : 0; - /* - * Rich players bullying poorer targets - * receive a success penalty. - */ - const bullyPenalty = wealthRatio > 1 @@ -1532,10 +1233,8 @@ function getStealChance( ) * 0.12, - 0.20 ) - : 0; @@ -1551,7 +1250,6 @@ function getStealChance( return Math.max( MIN_CHANCE, - Math.min( MAX_CHANCE, chance @@ -1587,7 +1285,6 @@ function getStealAmount( const amount = Math.max( 1, - Math.min( calculatedAmount, targetCookies, @@ -1614,11 +1311,6 @@ function getStealPercentage() { Math.random(); - /* - * 50%: - * 1–3% - */ - if ( roll < 0.50 @@ -1630,11 +1322,6 @@ function getStealPercentage() { } - /* - * 30%: - * 3–5% - */ - if ( roll < 0.80 @@ -1646,11 +1333,6 @@ function getStealPercentage() { } - /* - * 15%: - * 5–8% - */ - if ( roll < 0.95 @@ -1662,11 +1344,6 @@ function getStealPercentage() { } - /* - * 5% jackpot: - * 8–10% - */ - return randomFloat( 0.08, 0.10 @@ -1683,39 +1360,17 @@ function getStealPercentage() { function getLeaderboardMedal( index ) { - switch ( - 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; \ No newline at end of file +} \ No newline at end of file diff --git a/utils/crumbCollect.js b/utils/crumbCollect.js index 154a2c0..ea17c73 100644 --- a/utils/crumbCollect.js +++ b/utils/crumbCollect.js @@ -23,24 +23,159 @@ const { * ============================================================ */ -const COLLECT_COOLDOWN = - 60 * 60; - +const COLLECT_COOLDOWN = 60 * 60; +const COLLECT_MIN = 1; const COLLECT_MAX = 1000; -/* - * IMPORTANT: - * - * Always use exactly the same cooldown key. - */ - -const COLLECT_COOLDOWN_KEY = - "collect"; +const COLLECT_COOLDOWN_KEY = "collect"; /* * ============================================================ - * SHOW COLLECT PAGE + * SHARED COLLECT LOGIC + * ============================================================ + * + * This is the one authoritative implementation used by: + * + * - /cookies collect + * - Crumb Saucer Cookie Bakery + * + * It does not reply/update Discord itself. + */ + +async function collectCookies( + interaction, + userId = interaction.user.id +) { + const cooldown = await claimCooldown( + userId, + interaction.guildId, + COLLECT_COOLDOWN_KEY, + COLLECT_COOLDOWN + ); + + + if (!cooldown.claimed) { + const balance = await getCookieBalance( + userId + ); + + return { + collected: false, + cooldown, + balance, + amount: 0 + }; + } + + + const amount = + Math.floor( + Math.random() * + ( + COLLECT_MAX - + COLLECT_MIN + + 1 + ) + ) + + COLLECT_MIN; + + + await changeCookies( + userId, + amount + ); + + + await trackProfileEvent( + interaction, + userId, + { + type: "cookie_gain", + amount + } + ); + + + const balance = await getCookieBalance( + userId + ); + + + return { + collected: true, + cooldown, + balance, + amount + }; +} + + +/* + * ============================================================ + * SHARED EMBED BUILDERS + * ============================================================ + */ + +function buildCollectSuccessEmbed( + userId, + result, + footerType = "crumbSaucerCollect" +) { + return new EmbedBuilder() + .setTitle( + "🍪 Fresh Batch!" + ) + .setDescription( + [ + `You collected **${result.amount.toLocaleString()} Cookies**! 🍪`, + "", + `Your next batch will be ready .` + ].join("\n") + ) + .addFields({ + name: "Your Vault", + value: + `**${result.balance.toLocaleString()} 🍪**` + }) + .setFooter({ + text: + `${footerType}:${userId}` + }); +} + + +function buildCollectCooldownEmbed( + userId, + result, + footerType = "crumbSaucerCollect" +) { + return new EmbedBuilder() + .setTitle( + "🍪 Cookie Bakery" + ) + .setDescription( + [ + "Your next batch isn't ready yet.", + "", + `Come back .` + ].join("\n") + ) + .addFields({ + name: "Your Vault", + value: + `**${result.balance.toLocaleString()} 🍪**` + }) + .setFooter({ + text: + `${footerType}:${userId}` + }); +} + + +/* + * ============================================================ + * SHOW SAUCER COLLECT PAGE * ============================================================ */ @@ -78,7 +213,6 @@ async function showCollectPage( .setTitle( "🍪 Crumb Saucer Cookie Bakery" ) - .setDescription( [ "Fresh Cookies are prepared regularly.", @@ -88,15 +222,11 @@ async function showCollectPage( "Your collection cooldown is shared with `/cookies collect`." ].join("\n") ) - .addFields({ - name: - "Your Vault", - + name: "Your Vault", value: `**${balance.toLocaleString()} 🍪**` }) - .setFooter({ text: `crumbSaucerCollect:${interaction.user.id}` @@ -104,10 +234,7 @@ async function showCollectPage( return interaction.update({ - embeds: [ - embed - ], - + embeds: [embed], components: buildCollectButtons() }); @@ -116,7 +243,7 @@ async function showCollectPage( /* * ============================================================ - * COLLECT + * SAUCER COLLECT WRAPPER * ============================================================ */ @@ -143,154 +270,27 @@ async function collectCookiesFromSaucer( } - /* - * Mongo-backed cooldown. - * - * Same pattern as Cookie Heist: - * - * userId - * guildId - * cooldown key - * duration - */ - - const cooldown = - await claimCooldown( - interaction.user.id, - interaction.guildId, - COLLECT_COOLDOWN_KEY, - COLLECT_COOLDOWN - ); - - - /* - * Already collected. - */ - - if ( - !cooldown.claimed - ) { - const balance = - await getCookieBalance( - interaction.user.id - ); - - - const embed = - new EmbedBuilder() - .setTitle( - "🍪 Cookie Bakery" - ) - - .setDescription( - [ - "Your next batch isn't ready yet.", - "", - `Come back .` - ].join("\n") - ) - - .addFields({ - name: - "Your Vault", - - value: - `**${balance.toLocaleString()} 🍪**` - }) - - .setFooter({ - text: - `crumbSaucerCollect:${interaction.user.id}` - }); - - - return interaction.update({ - embeds: [ - embed - ], - - components: - buildCollectButtons() - }); - } - - - /* - * ======================================================== - * GENERATE BATCH - * ======================================================== - */ - - const amount = - Math.floor( - Math.random() * - COLLECT_MAX - ) - + - 1; - - - await changeCookies( - interaction.user.id, - amount - ); - - - /* - * Profile / achievements. - */ - - await trackProfileEvent( - interaction, - interaction.user.id, - { - type: - "cookie_gain", - - amount - } - ); - - - const balance = - await getCookieBalance( + const result = + await collectCookies( + interaction, interaction.user.id ); const embed = - new EmbedBuilder() - .setTitle( - "🍪 Fresh Batch!" + result.collected + ? buildCollectSuccessEmbed( + interaction.user.id, + result ) - - .setDescription( - [ - `You collected **${amount.toLocaleString()} Cookies**! 🍪`, - "", - `Your next batch will be ready .` - ].join("\n") - ) - - .addFields({ - name: - "Your Vault", - - value: - `**${balance.toLocaleString()} 🍪**` - }) - - .setFooter({ - text: - `crumbSaucerCollect:${interaction.user.id}` - }); + : buildCollectCooldownEmbed( + interaction.user.id, + result + ); return interaction.update({ - embeds: [ - embed - ], - + embeds: [embed], components: buildCollectButtons() }); @@ -361,10 +361,6 @@ function getCollectOwner( []; - /* - * Main menu -> Cookie Bakery. - */ - if ( parts[0] === "crumbSaucer" @@ -373,10 +369,6 @@ function getCollectOwner( } - /* - * Bakery page/result. - */ - if ( parts[0] === "crumbSaucerCollect" @@ -396,6 +388,15 @@ function getCollectOwner( */ module.exports = { + collectCookies, + + buildCollectSuccessEmbed, + buildCollectCooldownEmbed, + showCollectPage, - collectCookiesFromSaucer + collectCookiesFromSaucer, + + COLLECT_COOLDOWN, + COLLECT_MIN, + COLLECT_MAX }; \ No newline at end of file