const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js"); const { getCookieBalance, transferCookies, temporaryEphemeralReply } = require("./cookieEconomy"); const { trackProfileEvent } = require("./profileSystem"); const ROULETTE_BETS = [ 10, 50, 100, 500, 1000 ]; const ROULETTE_CHOICES = { Red: { name: "Red", emoji: "🔴", multiplier: 2 }, Black: { name: "Black", emoji: "âšĢ", multiplier: 2 }, Odd: { name: "Odd", emoji: "đŸ”ĸ", multiplier: 2 }, Even: { name: "Even", emoji: "2ī¸âƒŖ", multiplier: 2 }, Zero: { name: "Zero", emoji: "đŸŸĸ", multiplier: 36 } }; const RED_NUMBERS = new Set([ 1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, 36 ]); async function showRoulette( interaction ) { const ownerId = getRouletteSessionOwner( interaction ); if ( ownerId !== interaction.user.id ) { return temporaryEphemeralReply( interaction, { content: "This isn't your Crumb Saucer session." } ); } const balance = await getCookieBalance( interaction.user.id ); const embed = new EmbedBuilder() .setTitle( "🎡 Crumb Saucer Roulette" ) .setDescription( [ "European Roulette — **0 to 36**.", "", "Choose your wager.", "", "🔴 **Red** — x2", "âšĢ **Black** — x2", "đŸ”ĸ **Odd** — x2", "2ī¸âƒŖ **Even** — x2", "đŸŸĸ **Zero** — x36" ].join("\n") ) .addFields({ name: "Your Vault", value: `**${balance.toLocaleString()} đŸĒ**` }) .setFooter({ text: `crumbRouletteMenu:${interaction.user.id}` }); return interaction.update({ embeds: [ embed ], components: buildBetButtons() }); } async function chooseRouletteBet( interaction, bet ) { if ( !ROULETTE_BETS.includes( bet ) ) { return temporaryEphemeralReply( interaction, { content: "That Roulette wager isn't available." } ); } const ownerId = getRouletteSessionOwner( interaction ); if ( ownerId !== interaction.user.id ) { return temporaryEphemeralReply( interaction, { content: "This isn't your Roulette table." } ); } return showRouletteChoices( interaction, bet ); } async function startRouletteAllIn( interaction ) { const ownerId = getRouletteSessionOwner( interaction ); if ( ownerId !== interaction.user.id ) { return temporaryEphemeralReply( interaction, { content: "This isn't your Roulette table." } ); } const balance = await getCookieBalance( interaction.user.id ); if ( balance <= 0 ) { return temporaryEphemeralReply( interaction, { content: "You don't have any Cookies to go All In with." } ); } return showRouletteChoices( interaction, balance ); } async function showRouletteChoices( interaction, bet ) { if ( !Number.isSafeInteger( bet ) || bet <= 0 ) { return temporaryEphemeralReply( interaction, { content: "That Roulette wager isn't valid." } ); } const balance = await getCookieBalance( interaction.user.id ); if ( balance < bet ) { return temporaryEphemeralReply( interaction, { content: `You need **${bet.toLocaleString()} Cookies** for that wager.` } ); } const embed = new EmbedBuilder() .setTitle( "🎡 Crumb Saucer Roulette" ) .setDescription( [ `Wager: **${bet.toLocaleString()} đŸĒ**`, "", "Where do you want to place your Cookies?", "", "🔴 **Red** — x2", "âšĢ **Black** — x2", "đŸ”ĸ **Odd** — x2", "2ī¸âƒŖ **Even** — x2", "đŸŸĸ **Zero** — x36" ].join("\n") ) .addFields({ name: "Your Vault", value: `**${balance.toLocaleString()} đŸĒ**` }) .setFooter({ text: `crumbRouletteBet:${interaction.user.id}:${bet}` }); return interaction.update({ embeds: [ embed ], components: buildChoiceButtons() }); } async function runRouletteBack( interaction ) { const footer = interaction.message ?.embeds?.[0] ?.footer ?.text; const parts = footer?.split(":") ?? []; if ( parts[0] !== "crumbRouletteResult" ) { return temporaryEphemeralReply( interaction, { content: "This Roulette result can no longer be replayed." } ); } const ownerId = parts[1]; const bet = Number( parts[2] ); if ( ownerId !== interaction.user.id ) { return temporaryEphemeralReply( interaction, { content: "This isn't your Roulette session." } ); } if ( !Number.isSafeInteger( bet ) || bet <= 0 ) { return temporaryEphemeralReply( interaction, { content: "The previous wager is no longer valid." } ); } return showRouletteChoices( interaction, bet ); } async function playRoulette( interaction, choiceKey ) { const choice = ROULETTE_CHOICES[ choiceKey ]; if (!choice) { return temporaryEphemeralReply( interaction, { content: "That Roulette bet doesn't exist." } ); } const footer = interaction.message ?.embeds?.[0] ?.footer ?.text; const parts = footer?.split(":") ?? []; if ( parts[0] !== "crumbRouletteBet" ) { return temporaryEphemeralReply( interaction, { content: "This Roulette wager is no longer valid." } ); } const ownerId = parts[1]; const bet = Number( parts[2] ); if ( ownerId !== interaction.user.id ) { return temporaryEphemeralReply( interaction, { content: "This isn't your Roulette table." } ); } if ( !Number.isSafeInteger( bet ) || bet <= 0 ) { return temporaryEphemeralReply( interaction, { content: "That Roulette wager is invalid." } ); } const playerId = interaction.user.id; const houseId = interaction.client.user.id; const [ playerBalance, houseBalance ] = await Promise.all([ getCookieBalance( playerId ), getCookieBalance( houseId ) ]); if ( playerBalance < bet ) { return temporaryEphemeralReply( interaction, { content: "You no longer have enough Cookies for that wager." } ); } const maximumPayout = bet * choice.multiplier; if ( houseBalance + bet < maximumPayout ) { return temporaryEphemeralReply( interaction, { content: "The Crumb Saucer vault can't currently cover that Roulette wager." } ); } try { await transferCookies( playerId, houseId, bet ); } catch (error) { if ( error.message === "INSUFFICIENT_COOKIES" ) { return temporaryEphemeralReply( interaction, { content: "Your balance changed before the wheel could spin." } ); } throw error; } await interaction.update({ embeds: [ buildSpinEmbed( choice, bet, randomRouletteNumber(), "The wheel begins to spin..." ) ], components: [] }); await sleep( 650 ); await interaction.editReply({ embeds: [ buildSpinEmbed( choice, bet, randomRouletteNumber(), "Round and round..." ) ], components: [] }); await sleep( 650 ); await interaction.editReply({ embeds: [ buildSpinEmbed( choice, bet, randomRouletteNumber(), "The ball is slowing down..." ) ], components: [] }); await sleep( 750 ); const result = randomRouletteNumber(); const won = didRouletteBetWin( choiceKey, result ); const payout = won ? bet * choice.multiplier : 0; if ( payout > 0 ) { try { await transferCookies( houseId, playerId, payout ); } catch (error) { console.error( "[Roulette] Payout failed:", error ); return interaction.editReply({ embeds: [ new EmbedBuilder() .setTitle( "🎡 Roulette Error" ) .setDescription( "The wheel resolved, but the payout failed. Please contact an administrator." ) .setFooter({ text: `crumbRouletteResult:${playerId}:${bet}` }) ], components: buildResultButtons() }); } } const [ playerAfter, houseAfter ] = await Promise.all([ getCookieBalance( playerId ), getCookieBalance( houseId ) ]); const profit = payout - bet; await trackProfileEvent( interaction, playerId, { type: "game_result", game: "roulette", result: profit > 0 ? "win" : profit < 0 ? "loss" : "push", profit } ); if ( profit > 0 ) { await trackProfileEvent( interaction, playerId, { type: "cookie_gain", amount: profit } ); } else if ( profit < 0 ) { await trackProfileEvent( interaction, playerId, { type: "cookie_loss", amount: Math.abs( profit ) } ); } const resultEmbed = new EmbedBuilder() .setTitle( won ? "🎉 Roulette Winner!" : "🎡 Roulette" ) .setDescription( [ "## The ball lands on...", "", `# ${formatRouletteNumber(result)}`, "", `${choice.emoji} Your bet: **${choice.name}**`, "", won ? `🎉 **YOU WIN ${payout.toLocaleString()} đŸĒ!**` : "The house takes this one." ].join("\n") ) .addFields( { name: "Wager", value: `${bet.toLocaleString()} đŸĒ`, inline: true }, { name: "Payout", value: `${payout.toLocaleString()} đŸĒ`, inline: true }, { name: "Profit", value: `${profit >= 0 ? "+" : ""}${profit.toLocaleString()} đŸĒ`, inline: true }, { name: "Your Vault", value: `${playerAfter.toLocaleString()} đŸĒ`, inline: true }, { name: "House Vault", value: `${houseAfter.toLocaleString()} đŸĒ`, inline: true } ) .setFooter({ text: `crumbRouletteResult:${playerId}:${bet}` }); return interaction.editReply({ embeds: [ resultEmbed ], components: buildResultButtons() }); } function didRouletteBetWin( choice, number ) { switch ( choice ) { case "Red": return ( number !== 0 && RED_NUMBERS.has( number ) ); case "Black": return ( number !== 0 && !RED_NUMBERS.has( number ) ); case "Odd": return ( number !== 0 && number % 2 === 1 ); case "Even": return ( number !== 0 && number % 2 === 0 ); case "Zero": return ( number === 0 ); default: return false; } } function buildSpinEmbed( choice, bet, number, status ) { return new EmbedBuilder() .setTitle( "🎡 Crumb Saucer Roulette" ) .setDescription( [ "## 🎡 Spinning...", "", `### ${formatRouletteNumber(number)}`, "", `**${status}**` ].join("\n") ) .addFields( { name: "Bet", value: `${choice.emoji} ${choice.name}`, inline: true }, { name: "Wager", value: `${bet.toLocaleString()} đŸĒ`, inline: true } ); } function formatRouletteNumber( number ) { if ( number === 0 ) { return "đŸŸĸ 0"; } if ( RED_NUMBERS.has( number ) ) { return `🔴 ${number}`; } return `âšĢ ${number}`; } function buildBetButtons() { return [ new ActionRowBuilder() .addComponents( ...ROULETTE_BETS.map( bet => new ButtonBuilder() .setCustomId( `crumbRouletteBet${bet}` ) .setLabel( bet.toLocaleString() ) .setEmoji( "đŸĒ" ) .setStyle( bet >= 500 ? ButtonStyle.Primary : ButtonStyle.Secondary ) ) ), new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId( "crumbRouletteAllIn" ) .setLabel( "All In" ) .setEmoji( "đŸ”Ĩ" ) .setStyle( ButtonStyle.Danger ), new ButtonBuilder() .setCustomId( "crumbSaucerBack" ) .setLabel( "Back to Crumb Saucer" ) .setEmoji( "â—€ī¸" ) .setStyle( ButtonStyle.Secondary ) ) ]; } function buildChoiceButtons() { return [ new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId( "crumbRouletteRed" ) .setLabel( "Red" ) .setEmoji( "🔴" ) .setStyle( ButtonStyle.Danger ), new ButtonBuilder() .setCustomId( "crumbRouletteBlack" ) .setLabel( "Black" ) .setEmoji( "âšĢ" ) .setStyle( ButtonStyle.Secondary ), new ButtonBuilder() .setCustomId( "crumbRouletteOdd" ) .setLabel( "Odd" ) .setStyle( ButtonStyle.Primary ), new ButtonBuilder() .setCustomId( "crumbRouletteEven" ) .setLabel( "Even" ) .setStyle( ButtonStyle.Primary ), new ButtonBuilder() .setCustomId( "crumbRouletteZero" ) .setLabel( "Zero" ) .setEmoji( "đŸŸĸ" ) .setStyle( ButtonStyle.Success ) ), new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId( "crumbSaucerRoulette" ) .setLabel( "Change Wager" ) .setEmoji( "â†Šī¸" ) .setStyle( ButtonStyle.Secondary ) ) ]; } function buildResultButtons() { return [ new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId( "crumbRouletteRunItBack" ) .setLabel( "Run It Back" ) .setEmoji( "🔁" ) .setStyle( ButtonStyle.Success ), new ButtonBuilder() .setCustomId( "crumbSaucerRoulette" ) .setLabel( "Change Bet" ) .setEmoji( "🎡" ) .setStyle( ButtonStyle.Primary ), new ButtonBuilder() .setCustomId( "crumbSaucerBack" ) .setLabel( "Crumb Saucer" ) .setEmoji( "â—€ī¸" ) .setStyle( ButtonStyle.Secondary ) ) ]; } function getRouletteSessionOwner( interaction ) { const footer = interaction.message ?.embeds?.[0] ?.footer ?.text; const parts = footer?.split(":") ?? []; if ( parts[0] === "crumbSaucer" ) { return parts[1]; } if ( parts[0] === "crumbRouletteMenu" ) { return parts[1]; } if ( parts[0] === "crumbRouletteBet" ) { return parts[1]; } if ( parts[0] === "crumbRouletteResult" ) { return parts[1]; } return null; } function randomRouletteNumber() { return Math.floor( Math.random() * 37 ); } function sleep( ms ) { return new Promise( resolve => setTimeout( resolve, ms ) ); } module.exports = { showRoulette, chooseRouletteBet, startRouletteAllIn, showRouletteChoices, playRoulette, runRouletteBack };