const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } = require("discord.js"); const { randomUUID } = require("node:crypto"); const { getCollections, getCookieBalance, changeCookies, transferCookies, getCooldown, setCooldown, getUnixTime, randomInteger, randomFloat, randomItem, formatPercentage } = require("../../utils/cookieEconomy"); /* * CONFIG */ const COLLECT_COOLDOWN = 60 * 60; const STEAL_COOLDOWN = 15 * 60; const SLOT_MIN_BET = 10; const SLOT_MAX_BET = 1000; const BOSS_SPAWN_COST = 500; const BOSS_JOIN_DURATION = 30 * 60; const BOSS_MINIMUM_PLAYERS = 5; const ABRA_ID = "378135175059603457"; const ABRA_MULTIPLIER = 1.5; /* * SLOT TABLE * * RTP ≈ 94% */ const SLOT_OUTCOMES = [ { weight: 300, type: "loss", multiplier: 0 }, { weight: 390, type: "pair", multiplier: 0.5 }, { weight: 150, type: "triple", symbol: "🍋", multiplier: 1.5 }, { weight: 80, type: "triple", symbol: "🍒", multiplier: 2 }, { weight: 45, type: "triple", symbol: "🍪", multiplier: 3 }, { weight: 25, type: "triple", symbol: "⭐", multiplier: 5 }, { weight: 10, type: "triple", symbol: "💎", multiplier: 10 } ]; const SLOT_SYMBOLS = [ "🍒", "🍋", "🍪", "⭐", "💎" ]; 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("heist") .setDescription( "Attempt to breach the ShaiBot Cookie Vault." ) ) .addSubcommand(subcommand => subcommand .setName("slots") .setDescription( "Play the Shaiwase Cookie Slots." ) .addIntegerOption(option => option .setName("bet") .setDescription( "How many Cookies to wager." ) .setRequired(true) .setMinValue( SLOT_MIN_BET ) .setMaxValue( SLOT_MAX_BET ) ) ) .addSubcommandGroup(group => group .setName("boss") .setDescription( "Cookie Boss commands." ) .addSubcommand(subcommand => subcommand .setName("spawn") .setDescription( `Spawn a Cookie Boss for ${BOSS_SPAWN_COST} Cookies.` ) ) ) .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) { const group = interaction.options .getSubcommandGroup(false); const subcommand = interaction.options .getSubcommand(); if ( group === "boss" && subcommand === "spawn" ) { return spawnBoss( interaction ); } switch (subcommand) { case "collect": return collect( interaction ); case "steal": return steal( interaction ); case "heist": return heist( interaction ); case "slots": return slots( interaction ); case "check": return check( interaction ); case "give": return give( interaction ); case "leaderboard": return leaderboard( interaction ); } } }; /* * COLLECT */ async function collect(interaction) { const userId = interaction.user.id; const now = getUnixTime(); const cooldown = await getCooldown( userId, interaction.guildId, "collect" ); if (cooldown > now) { return interaction.reply({ content: `The batch is still cooking. Come back 🍪`, flags: MessageFlags.Ephemeral }); } let amount = randomInteger( 1, 20 ); if ( userId === ABRA_ID ) { amount = Math.ceil( amount * ABRA_MULTIPLIER ); } await changeCookies( userId, amount ); await setCooldown( userId, interaction.guildId, "collect", COLLECT_COOLDOWN ); const balance = await getCookieBalance( userId ); const embed = new EmbedBuilder() .setTitle( "🍪 Fresh Batch" ) .setDescription( randomCollectResponse( amount, interaction.user ) ) .addFields({ name: "Your Vault", value: `**${balance.toLocaleString()} Cookies**` }); return interaction.reply({ embeds: [ embed ] }); } /* * NORMAL PLAYER STEAL */ async function steal(interaction) { const user = interaction.user; const target = interaction.options.getUser( "target", true ); if ( target.id === user.id ) { return interaction.reply({ content: "You can't steal from yourself.", flags: MessageFlags.Ephemeral }); } /* * ShaiBot itself is reserved for /cookies heist. */ if ( target.id === interaction.client.user.id ) { return interaction.reply({ content: "The ShaiBot Vault requires a proper `/cookies heist`.", flags: MessageFlags.Ephemeral }); } if (target.bot) { return interaction.reply({ content: "Other bots are protected by Cookie Law.", flags: MessageFlags.Ephemeral }); } const now = getUnixTime(); const cooldown = await getCooldown( user.id, interaction.guildId, "steal" ); if (cooldown > now) { return interaction.reply({ content: `You can steal again .`, 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: `${target} doesn't have any Cookies.`, flags: MessageFlags.Ephemeral }); } const successChance = getStealChance( yourCookies, theirCookies ); const success = Math.random() < successChance; let embed; if (success) { const { amount } = getStealAmount( theirCookies ); await transferCookies( target.id, user.id, amount ); const actualPercentage = ( amount / theirCookies * 100 ); embed = new EmbedBuilder() .setTitle( "🥷 Cookie Robbery Successful" ) .setDescription( randomStealSuccessResponse( amount, target ) ) .addFields( { name: "Vault Taken", value: `**${actualPercentage.toFixed(1)}%**`, inline: true }, { name: "Success Chance", value: `**${formatPercentage(successChance)}**`, inline: true } ); } else { const penaltyPercentage = randomFloat( 0.01, 0.08 ); const penalty = Math.max( 1, Math.min( yourCookies, Math.floor( yourCookies * penaltyPercentage ) ) ); /* * Normal steal failure pays the victim. */ await transferCookies( user.id, target.id, penalty ); embed = new EmbedBuilder() .setTitle( "🚨 Cookie Robbery Failed" ) .setDescription( randomStealFailResponse( penalty, target ) ) .addFields( { name: "Success Chance", value: `**${formatPercentage(successChance)}**`, inline: true } ); } const nextSteal = await setCooldown( user.id, interaction.guildId, "steal", STEAL_COOLDOWN ); embed.addFields({ name: "Next Robbery", value: `` }); return interaction.reply({ embeds: [ embed ] }); } /* * HEIST ENTRY */ async function heist(interaction) { const vaultId = interaction.client.user.id; const [ vault, player ] = await Promise.all([ getCookieBalance( vaultId ), getCookieBalance( interaction.user.id ) ]); if (player < 1) { return interaction.reply({ content: "You need Cookies before attempting a vault heist.", flags: MessageFlags.Ephemeral }); } if (vault < 1) { return interaction.reply({ content: "The ShaiBot Vault is currently empty.", flags: MessageFlags.Ephemeral }); } const security = getSecurityLabel( vault ); const embed = new EmbedBuilder() .setTitle( "🏦 ShaiBot Cookie Vault" ) .setDescription( [ `**Vault:** ${vault.toLocaleString()} 🍪`, `**Security:** ${security}`, "", "Choose your approach.", "", "🥷 **Sneak** — safer, smaller haul.", "💻 **Hack** — balanced risk and reward.", "💥 **Smash** — dangerous, but lucrative." ].join("\n") ) .setFooter({ text: `cookieHeist:${interaction.user.id}` }); const row = new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId( "cookieHeistSneak" ) .setLabel( "Sneak" ) .setEmoji( "🥷" ) .setStyle( ButtonStyle.Primary ), new ButtonBuilder() .setCustomId( "cookieHeistHack" ) .setLabel( "Hack" ) .setEmoji( "💻" ) .setStyle( ButtonStyle.Secondary ), new ButtonBuilder() .setCustomId( "cookieHeistSmash" ) .setLabel( "Smash" ) .setEmoji( "💥" ) .setStyle( ButtonStyle.Danger ) ); return interaction.reply({ embeds: [ embed ], components: [ row ] }); } /* * SLOTS */ async function slots(interaction) { const bet = interaction.options.getInteger( "bet", true ); const playerId = interaction.user.id; const vaultId = interaction.client.user.id; const [ playerBalance, vaultBalance ] = await Promise.all([ getCookieBalance( playerId ), getCookieBalance( vaultId ) ]); if ( playerBalance < bet ) { return interaction.reply({ content: "You don't have enough Cookies for that bet.", flags: MessageFlags.Ephemeral }); } /* * The house needs enough BEFORE the wager * to honor a 10x payout after receiving the bet. */ if ( vaultBalance < bet * 9 ) { return interaction.reply({ content: "The ShaiBot Vault doesn't currently have enough liquidity for that wager.", flags: MessageFlags.Ephemeral }); } await transferCookies( playerId, vaultId, bet ); const spinning = new EmbedBuilder() .setTitle( "🎰 Shaiwase Cookie Slots" ) .setDescription( buildSlotDisplay( generateRandomGrid() ) ) .addFields({ name: "Bet", value: `**${bet.toLocaleString()} 🍪**` }); await interaction.reply({ embeds: [ spinning ] }); await sleep(750); await interaction.editReply({ embeds: [ EmbedBuilder .from(spinning) .setDescription( buildSlotDisplay( generateRandomGrid() ) ) ] }); await sleep(750); await interaction.editReply({ embeds: [ EmbedBuilder .from(spinning) .setDescription( buildSlotDisplay( generateRandomGrid() ) ) ] }); await sleep(750); const outcome = rollSlotOutcome(); const grid = generateOutcomeGrid( outcome ); const payout = Math.floor( bet * outcome.multiplier ); if (payout > 0) { await transferCookies( vaultId, playerId, payout ); } const [ finalPlayer, finalVault ] = await Promise.all([ getCookieBalance( playerId ), getCookieBalance( vaultId ) ]); const profit = payout - bet; const finalEmbed = new EmbedBuilder() .setTitle( "🎰 Shaiwase Cookie Slots" ) .setDescription( buildSlotDisplay( grid ) ) .addFields( { name: "Result", value: getSlotResultName( outcome ) }, { name: "Bet", value: `${bet.toLocaleString()} 🍪`, inline: true }, { name: "Multiplier", value: `×${outcome.multiplier}`, inline: true }, { name: "Payout", value: `${payout.toLocaleString()} 🍪`, inline: true }, { name: "Profit", value: `${profit >= 0 ? "+" : ""}${profit.toLocaleString()} 🍪`, inline: true }, { name: "Your Vault", value: `${finalPlayer.toLocaleString()} 🍪`, inline: true }, { name: "House Vault", value: `${finalVault.toLocaleString()} 🍪`, inline: true } ); return interaction.editReply({ embeds: [ finalEmbed ] }); } /* * BOSS SPAWN */ async function spawnBoss(interaction) { const { db } = getCollections(); const events = db.collection( "items_events" ); /* * One unresolved boss per guild. */ const existing = await events.findOne({ eventType: "CookieBoss", guildId: interaction.guildId, state: { $in: [ "Joining", "Active", "Starting", "Resolving" ] } }); if (existing) { return interaction.reply({ content: "There is already an active Cookie Boss in this server.", flags: MessageFlags.Ephemeral }); } const balance = await getCookieBalance( interaction.user.id ); if ( balance < BOSS_SPAWN_COST ) { return interaction.reply({ content: `You need **${BOSS_SPAWN_COST} Cookies** to summon the boss.`, flags: MessageFlags.Ephemeral }); } const eventId = randomUUID(); const escrowId = `boss:${eventId}`; const now = getUnixTime(); const joinUntil = now + BOSS_JOIN_DURATION; /* * Spawner fee seeds the encounter pool. */ await transferCookies( interaction.user.id, escrowId, BOSS_SPAWN_COST ); const embed = new EmbedBuilder() .setTitle( "🍪 The Crumbbound Behemoth Appears!" ) .setDescription( [ "*Drawn by the scent of an irresponsibly large Cookie Vault,", "the Crumbbound Behemoth has lumbered into Shaiwase.*", "", `**Recruitment ends:** `, `**Minimum adventurers:** ${BOSS_MINIMUM_PLAYERS}`, `**Reward Pool:** ${BOSS_SPAWN_COST.toLocaleString()} 🍪`, "", "Choose how many Cookies you want to commit to the fight.", "Your entry tier determines your temporary HP and Power." ].join("\n") ) .setFooter({ text: `cookieBoss:${eventId}` }); const joinRow = new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId( "cookieBossJoinScout" ) .setLabel( "Scout — 100" ) .setEmoji( "🍪" ) .setStyle( ButtonStyle.Secondary ), new ButtonBuilder() .setCustomId( "cookieBossJoinFighter" ) .setLabel( "Fighter — 500" ) .setEmoji( "⚔️" ) .setStyle( ButtonStyle.Primary ), new ButtonBuilder() .setCustomId( "cookieBossJoinChampion" ) .setLabel( "Champion — 1000" ) .setEmoji( "🔥" ) .setStyle( ButtonStyle.Danger ) ); const startRow = new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId( "cookieBossStart" ) .setLabel( "Start Battle" ) .setEmoji( "⚔️" ) .setStyle( ButtonStyle.Success ) ); const message = await interaction.reply({ embeds: [ embed ], components: [ joinRow, startRow ], fetchReply: true }); await events.insertOne({ eventType: "CookieBoss", eventId, escrowId, guildId: interaction.guildId, channelId: interaction.channelId, messageId: message.id, creatorId: interaction.user.id, state: "Joining", createdAt: now, joinUntil, battleStartedAt: null, battleEndsAt: null, minimumParticipants: BOSS_MINIMUM_PLAYERS, rewardPool: BOSS_SPAWN_COST, boss: { bossId: "crumbbound-behemoth", name: "The Crumbbound Behemoth", health: 15000, maxHealth: 15000, rage: 0 }, participants: {} }); } /* * 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()} Cookies**.` ); return interaction.reply({ embeds: [ embed ] }); } /* * 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: "Use the Cookie games if you want to feed the house.", flags: MessageFlags.Ephemeral }); } try { await transferCookies( user.id, target.id, amount ); } catch (error) { if ( error.message === "INSUFFICIENT_COOKIES" ) { return interaction.reply({ content: "You don't have enough Cookies.", flags: MessageFlags.Ephemeral }); } throw error; } const embed = new EmbedBuilder() .setTitle( "🍪 Cookies Shared" ) .setDescription( `${user} gave ${target} **${amount.toLocaleString()} Cookies**.` ); return interaction.reply({ embeds: [ embed ] }); } /* * LEADERBOARD */ async function leaderboard(interaction) { const { cookies } = getCollections(); /* * Only Discord snowflake-style account IDs. * * This prevents boss escrow accounts from appearing. */ const results = await cookies .find({ userId: { $regex: /^\d+$/ }, cookies: { $gt: 0 } }) .sort({ cookies: -1 }) .limit(10) .toArray(); const description = results.length ? results .map( ( record, index ) => { return ( `${getLeaderboardMedal(index)} ` + `<@${record.userId}> — ` + `**${record.cookies.toLocaleString()}** 🍪` ); } ) .join("\n") : "Nobody has collected any Cookies yet."; const embed = new EmbedBuilder() .setTitle( "🍪 Cookie Leaderboard" ) .setDescription( description ) .setTimestamp(); return interaction.reply({ embeds: [ embed ] }); } /* * STEAL HELPERS */ function getStealChance( attackerCookies, targetCookies ) { const MIN_CHANCE = 0.15; const MAX_CHANCE = 0.70; const BASE_CHANCE = 0.50; const targetPenalty = Math.log10( targetCookies + 1 ) * 0.06; const wealthRatio = attackerCookies / Math.max( targetCookies, 1 ); const povertyBonus = wealthRatio < 1 ? ( 1 - wealthRatio ) * 0.18 : 0; const bullyPenalty = wealthRatio > 1 ? Math.min( Math.log10( wealthRatio + 1 ) * 0.12, 0.20 ) : 0; return Math.max( MIN_CHANCE, Math.min( MAX_CHANCE, BASE_CHANCE - targetPenalty + povertyBonus - bullyPenalty ) ); } function getStealAmount( targetCookies ) { const MAX_STEAL_AMOUNT = 5000; const percentage = getStealPercentage(); const amount = Math.max( 1, Math.min( Math.floor( targetCookies * percentage ), targetCookies, MAX_STEAL_AMOUNT ) ); return { amount, percentage }; } function getStealPercentage() { const roll = Math.random(); if (roll < 0.50) return randomFloat( 0.01, 0.03 ); if (roll < 0.80) return randomFloat( 0.03, 0.05 ); if (roll < 0.95) return randomFloat( 0.05, 0.08 ); return randomFloat( 0.08, 0.10 ); } /* * SLOT HELPERS */ function rollSlotOutcome() { const total = SLOT_OUTCOMES.reduce( ( sum, outcome ) => sum + outcome.weight, 0 ); let roll = Math.random() * total; for ( const outcome of SLOT_OUTCOMES ) { roll -= outcome.weight; if (roll <= 0) return outcome; } return SLOT_OUTCOMES[0]; } function generateRandomGrid() { return Array.from( { length: 3 }, () => Array.from( { length: 3 }, () => randomItem( SLOT_SYMBOLS ) ) ); } function generateOutcomeGrid( outcome ) { const grid = generateRandomGrid(); if ( outcome.type === "triple" ) { grid[1] = [ outcome.symbol, outcome.symbol, outcome.symbol ]; return grid; } if ( outcome.type === "pair" ) { const symbol = randomItem( SLOT_SYMBOLS ); let other; do { other = randomItem( SLOT_SYMBOLS ); } while ( other === symbol ); grid[1] = [ symbol, symbol, other ]; return grid; } /* * Guaranteed non-pair middle line. */ let line; do { line = [ randomItem( SLOT_SYMBOLS ), randomItem( SLOT_SYMBOLS ), randomItem( SLOT_SYMBOLS ) ]; } while ( new Set(line).size < 3 ); grid[1] = line; return grid; } function buildSlotDisplay( grid ) { return [ "```", `${grid[0][0]} │ ${grid[0][1]} │ ${grid[0][2]}`, `${grid[1][0]} │ ${grid[1][1]} │ ${grid[1][2]}`, `${grid[2][0]} │ ${grid[2][1]} │ ${grid[2][2]}`, "```" ].join("\n"); } function getSlotResultName( outcome ) { if ( outcome.type === "loss" ) { return "No match"; } if ( outcome.type === "pair" ) { return "Pair"; } switch ( outcome.symbol ) { case "🍋": return "Triple Lemon"; case "🍒": return "Triple Cherry"; case "🍪": return "Triple Cookie"; case "⭐": return "Triple Star"; case "💎": return "💎 JACKPOT 💎"; default: return "Triple"; } } function getSecurityLabel( vault ) { if (vault < 1000) return "LOW"; if (vault < 10000) return "MODERATE"; if (vault < 50000) return "HIGH"; if (vault < 250000) return "VERY HIGH"; return "MAXIMUM"; } /* * TEXT */ function randomCollectResponse( amount, user ) { return randomItem([ `${user}, I made you **${amount} Cookies** 🍪`, `${user}, here's **${amount} Cookies**. I ate the rest.`, `${user}, your **${amount} Cookies** are fresh out of the oven.`, `A wild baker appeared and handed ${user} **${amount} Cookies**.`, `${user}, Abra insists these **${amount} Cookies** are totally legitimate.` ]); } function randomStealSuccessResponse( amount, target ) { return randomItem([ `You stole **${amount.toLocaleString()} Cookies** from ${target}!`, `Clean getaway. ${target} is **${amount.toLocaleString()} Cookies** poorer.`, `The Cookie Vault was breached for **${amount.toLocaleString()} Cookies**.` ]); } function randomStealFailResponse( amount, target ) { return randomItem([ `${target} caught you and took **${amount.toLocaleString()} Cookies**.`, `The robbery failed. ${target} gained **${amount.toLocaleString()} Cookies**.`, `Cookie Police confiscated **${amount.toLocaleString()} Cookies** and handed them to ${target}.` ]); } function getLeaderboardMedal( index ) { switch (index) { case 0: return "🥇"; case 1: return "🥈"; case 2: return "🥉"; default: return `**${index + 1}.**`; } } function sleep(ms) { return new Promise( resolve => setTimeout( resolve, ms ) ); }