const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js"); const { getCookieBalance, transferCookies, claimCooldown, temporaryEphemeralReply, randomFloat, formatPercentage } = require("./cookieEconomy"); /* * ============================================================ * CONFIG * ============================================================ */ const HEIST_COOLDOWN = 60 * 60; // 1 hour /* * All three approaches use the SAME cooldown: * * cooldowns_cookies.heist * * Sneak does NOT have its own cooldown. * Hack does NOT have its own cooldown. * Smash does NOT have its own cooldown. */ const HEIST_METHODS = { Sneak: { baseChance: 0.65, minStealPercent: 0.01, maxStealPercent: 0.03, maxStealAmount: 1500, minPenaltyPercent: 0.01, maxPenaltyPercent: 0.04 }, Hack: { baseChance: 0.45, minStealPercent: 0.03, maxStealPercent: 0.06, maxStealAmount: 3500, minPenaltyPercent: 0.03, maxPenaltyPercent: 0.07 }, Smash: { baseChance: 0.25, minStealPercent: 0.06, maxStealPercent: 0.12, maxStealAmount: 7500, minPenaltyPercent: 0.06, maxPenaltyPercent: 0.12 } }; /* * ============================================================ * RESOLVE HEIST * ============================================================ */ async function resolveCookieHeist( interaction, methodName ) { /* * Validate ownership of this Heist menu. * * Footer: * * cookieHeist: */ const footer = interaction.message ?.embeds?.[0] ?.footer ?.text; const [ type, ownerId ] = footer?.split(":") ?? []; if ( type !== "cookieHeist" || ownerId !== interaction.user.id ) { return temporaryEphemeralReply( interaction, { content: "This isn't your heist. Open your own from `/crumbsaucer`." } ); } /* * Validate method. */ const method = HEIST_METHODS[ methodName ]; if (!method) { return temporaryEphemeralReply( interaction, { content: "That heist approach doesn't exist." } ); } /* * ======================================================== * SHARED HEIST COOLDOWN * ======================================================== */ const cooldown = await claimCooldown( interaction.user.id, interaction.guildId, "heist", HEIST_COOLDOWN ); if ( !cooldown.claimed ) { return temporaryEphemeralReply( interaction, { content: `🏦 The vault is still on high alert.\nYou can attempt another heist .` }, 6000 ); } /* * Lock this Heist interaction immediately. */ await interaction.update({ embeds: [ new EmbedBuilder() .setTitle( "🏦 Vault Heist in Progress..." ) .setDescription( getHeistProgressText( methodName ) ) .setFooter({ text: `cookieHeist:${interaction.user.id}` }) ], components: [] }); const playerId = interaction.user.id; const vaultId = interaction.client.user.id; /* * Re-fetch balances because they may have * changed since the menu was opened. */ const [ playerCookies, vaultCookies ] = await Promise.all([ getCookieBalance( playerId ), getCookieBalance( vaultId ) ]); if ( playerCookies < 1 || vaultCookies < 1 ) { return interaction.editReply({ embeds: [ new EmbedBuilder() .setTitle( "🏦 Heist Aborted" ) .setDescription( "There weren't enough Cookies available to resolve the heist." ) .addFields({ name: "Next Heist", value: `` }) .setFooter({ text: `cookieHeist:${playerId}` }) ], components: buildReturnButton() }); } /* * Short theatrical delay. */ await sleep( 1200 ); /* * ======================================================== * DYNAMIC VAULT SECURITY * ======================================================== */ const securityPenalty = getVaultSecurityPenalty( vaultCookies ); const successChance = Math.max( 0.10, method.baseChance - securityPenalty ); const success = Math.random() < successChance; /* * ======================================================== * SUCCESS * ======================================================== */ if (success) { const rolledPercent = randomFloat( method.minStealPercent, method.maxStealPercent ); const amount = Math.max( 1, Math.min( Math.floor( vaultCookies * rolledPercent ), method.maxStealAmount, vaultCookies ) ); try { await transferCookies( vaultId, playerId, amount ); } catch (error) { if ( error.message === "INSUFFICIENT_COOKIES" ) { return interaction.editReply({ embeds: [ new EmbedBuilder() .setTitle( "🏦 Heist Interrupted" ) .setDescription( "The ShaiBot Vault changed before you could complete the robbery." ) .addFields({ name: "Next Heist", value: `` }) .setFooter({ text: `cookieHeist:${playerId}` }) ], components: buildReturnButton() }); } throw error; } const [ playerAfter, vaultAfter ] = await Promise.all([ getCookieBalance( playerId ), getCookieBalance( vaultId ) ]); const actualPercentage = ( amount / vaultCookies ) * 100; const embed = new EmbedBuilder() .setTitle( "💥 Vault Breached!" ) .setDescription( getSuccessText( methodName, interaction.user, amount ) ) .addFields( { name: "Approach", value: methodName, inline: true }, { name: "Success Chance", value: `**${formatPercentage(successChance)}**`, inline: true }, { name: "Vault Taken", value: `**${actualPercentage.toFixed(2)}%**`, inline: true }, { name: "Cookies Stolen", value: `**${amount.toLocaleString()} 🍪**`, inline: true }, { name: "Your Vault", value: `**${playerAfter.toLocaleString()} 🍪**`, inline: true }, { name: "House Vault", value: `**${vaultAfter.toLocaleString()} 🍪**`, inline: true }, { name: "Next Heist", value: ``, inline: false } ) .setFooter({ text: `cookieHeist:${playerId}` }); return interaction.editReply({ embeds: [ embed ], components: buildReturnButton() }); } /* * ======================================================== * FAILURE * ======================================================== */ const penaltyPercent = randomFloat( method.minPenaltyPercent, method.maxPenaltyPercent ); const penalty = Math.max( 1, Math.min( Math.floor( playerCookies * penaltyPercent ), playerCookies ) ); try { /* * Heist fines always go into * ShaiBot's central vault. */ await transferCookies( playerId, vaultId, penalty ); } catch (error) { if ( error.message === "INSUFFICIENT_COOKIES" ) { return interaction.editReply({ embeds: [ new EmbedBuilder() .setTitle( "🚨 Heist Failed" ) .setDescription( "Your Cookie balance changed before the fine could be collected." ) .addFields({ name: "Next Heist", value: `` }) .setFooter({ text: `cookieHeist:${playerId}` }) ], components: buildReturnButton() }); } throw error; } const [ playerAfter, vaultAfter ] = await Promise.all([ getCookieBalance( playerId ), getCookieBalance( vaultId ) ]); const actualPenaltyPercentage = ( penalty / playerCookies ) * 100; const embed = new EmbedBuilder() .setTitle( "🚨 HEIST FAILED" ) .setDescription( getFailureText( methodName, interaction.user, penalty ) ) .addFields( { name: "Approach", value: methodName, inline: true }, { name: "Success Chance", value: `**${formatPercentage(successChance)}**`, inline: true }, { name: "Fine", value: `**${penalty.toLocaleString()} 🍪**`, inline: true }, { name: "Vault Lost", value: `**${actualPenaltyPercentage.toFixed(1)}%**`, inline: true }, { name: "Your Vault", value: `**${playerAfter.toLocaleString()} 🍪**`, inline: true }, { name: "House Vault", value: `**${vaultAfter.toLocaleString()} 🍪**`, inline: true }, { name: "Next Heist", value: ``, inline: false } ) .setFooter({ text: `cookieHeist:${playerId}` }); return interaction.editReply({ embeds: [ embed ], components: buildReturnButton() }); } /* * ============================================================ * RETURN TO CRUMB SAUCER * ============================================================ */ function buildReturnButton() { return [ new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId( "crumbSaucerBack" ) .setLabel( "Back to Crumb Saucer" ) .setEmoji( "◀️" ) .setStyle( ButtonStyle.Secondary ) ) ]; } /* * ============================================================ * SECURITY * ============================================================ */ function getVaultSecurityPenalty( vaultCookies ) { /* * Approximately: * * 100 -> 5% * 1,000 -> 7.5% * 10,000 -> 10% * 100,000 -> 12.5% * 1,000,000 -> 15% */ return Math.min( Math.log10( vaultCookies + 1 ) * 0.025, 0.15 ); } /* * ============================================================ * PROGRESS TEXT * ============================================================ */ function getHeistProgressText( methodName ) { switch ( methodName ) { case "Sneak": return [ "🥷 You slip through the service entrance...", "", "The Cookie Vault guards haven't noticed you yet." ].join("\n"); case "Hack": return [ "💻 You connect to the vault terminal...", "", "Several extremely suspicious progress bars appear." ].join("\n"); case "Smash": return [ "💥 You charge directly at the vault...", "", "Subtlety has been abandoned." ].join("\n"); default: return "The heist is underway..."; } } /* * ============================================================ * SUCCESS TEXT * ============================================================ */ function getSuccessText( methodName, user, amount ) { const cookies = `**${amount.toLocaleString()} Cookies**`; switch ( methodName ) { case "Sneak": return ( `🥷 ${user} slipped through security and quietly escaped with ${cookies}.` ); case "Hack": return ( `💻 ${user} convinced the vault terminal that ${cookies} belonged to them.` ); case "Smash": return ( `💥 ${user} smashed through the Cookie Vault and escaped with ${cookies}!` ); default: return ( `${user} escaped with ${cookies}.` ); } } /* * ============================================================ * FAILURE TEXT * ============================================================ */ function getFailureText( methodName, user, penalty ) { const cookies = `**${penalty.toLocaleString()} Cookies**`; switch ( methodName ) { case "Sneak": return [ `🚨 ${user} stepped directly on the world's loudest Cookie wrapper.`, "", `The ShaiBot Vault collected a ${cookies} fine.` ].join("\n"); case "Hack": return [ `🚨 ${user} entered the password incorrectly one too many times.`, "", `The ShaiBot Vault collected a ${cookies} security fee.` ].join("\n"); case "Smash": return [ `🚨 ${user} discovered that the vault door is significantly stronger than expected.`, "", `Repairs cost ${cookies}. Naturally, you're paying.` ].join("\n"); default: return ( `The heist failed and cost ${user} ${cookies}.` ); } } /* * ============================================================ * UTIL * ============================================================ */ function sleep( ms ) { return new Promise( resolve => setTimeout( resolve, ms ) ); } /* * ============================================================ * EXPORTS * ============================================================ */ module.exports = { resolveCookieHeist, HEIST_COOLDOWN };