const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js"); const { getCookieBalance, transferCookies, temporaryEphemeralReply, randomItem } = require("./cookieEconomy"); const { recordGameOutcome, trackProfileEvent } = require("./profileSystem"); const SLOT_BETS = [ 10, 50, 100, 500, 1000 ]; const SLOT_SYMBOLS = [ "🍒", "🍋", "🍪", "⭐", "💎" ]; 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 } ]; async function showSlots(interaction) { const ownerId = getSlotsSessionOwner( 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 Slots" ) .setDescription( [ "Welcome to the Cookie reels.", "", "Choose your wager below.", "", "🍋 **Triple Lemon** — x1.5", "🍒 **Triple Cherry** — x2", "🍪 **Triple Cookie** — x3", "⭐ **Triple Star** — x5", "💎 **Jackpot** — x10" ].join("\n") ) .addFields({ name: "Your Vault", value: `**${balance.toLocaleString()} 🍪**` }) .setFooter({ text: `crumbSaucerSlots:${interaction.user.id}` }); return interaction.update({ embeds: [embed], components: buildSlotBetButtons() }); } async function playSlots( interaction, bet, source = "normal", previousAllIn = false, previousBet = 0 ) { const ownerId = getSlotsSessionOwner( interaction ); if ( ownerId !== interaction.user.id ) { return temporaryEphemeralReply( interaction, { content: "These aren't your slots. Open your own Crumb Saucer session." } ); } if (!SLOT_BETS.includes(bet)) { return temporaryEphemeralReply( interaction, { content: "That wager isn't available." } ); } 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 temporaryEphemeralReply( interaction, { content: `You need **${bet.toLocaleString()} Cookies** for that wager.` } ); } const maxPayout = bet * 10; if ( vaultBalance + bet < maxPayout ) { return temporaryEphemeralReply( interaction, { content: "The Crumb Saucer vault doesn't currently have enough Cookies to cover that wager." } ); } try { await transferCookies( playerId, vaultId, bet ); } catch (error) { if ( error.message === "INSUFFICIENT_COOKIES" ) { return temporaryEphemeralReply( interaction, { content: "Your Cookie balance changed before the spin could begin." } ); } throw error; } await trackProfileEvent( interaction, playerId, { type: "game_start", game: "slots", source, wager: bet, allIn: bet === playerBalance, previousAllIn, previousBet } ); await interaction.update({ embeds: [ buildSpinningEmbed( bet, ["❔", "❔", "❔"], "The reels are spinning..." ) ], components: [] }); await sleep(500); const firstPreview = randomItem(SLOT_SYMBOLS); await interaction.editReply({ embeds: [ buildSpinningEmbed( bet, [ firstPreview, "❔", "❔" ], "The first reel stops..." ) ], components: [] }); await sleep(500); const secondPreview = randomItem(SLOT_SYMBOLS); await interaction.editReply({ embeds: [ buildSpinningEmbed( bet, [ firstPreview, secondPreview, "❔" ], "One more..." ) ], components: [] }); await sleep(600); const outcome = rollSlotOutcome(); const result = generateOutcomeLine( outcome ); const payout = Math.floor( bet * outcome.multiplier ); if (payout > 0) { try { await transferCookies( vaultId, playerId, payout ); } catch (error) { console.error( "CRITICAL: Crumb Saucer Slots payout failed:", error ); return interaction.editReply({ embeds: [ new EmbedBuilder() .setTitle( "🎰 Crumb Saucer Error" ) .setDescription( "The reels stopped, but the payout system had a problem. Please contact an administrator." ) .setFooter({ text: `crumbSaucerSlotsResult:${playerId}:${bet}` }) ], components: buildResultButtons() }); } } const [ playerAfter, vaultAfter ] = await Promise.all([ getCookieBalance(playerId), getCookieBalance(vaultId) ]); const profit = payout - bet; await recordGameOutcome( interaction, playerId, { game: "slots", result: profit > 0 ? "win" : profit < 0 ? "loss" : "push", profit, triplet: outcome.type === "triple" ? outcome.symbol : null, jackpot: outcome.type === "triple" && outcome.symbol === "💎" } ); const resultEmbed = new EmbedBuilder() .setTitle( "🎰 Crumb Saucer Slots" ) .setDescription( [ "## " + result.join(" "), "", `### ${getSlotResultName(outcome)}` ].join("\n") ) .addFields( { name: "Bet", value: `${bet.toLocaleString()} 🍪`, inline: true }, { name: "Multiplier", value: `x${outcome.multiplier}`, 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: `${vaultAfter.toLocaleString()} 🍪`, inline: true } ) .setFooter({ text: `crumbSaucerSlotsResult:${playerId}:${bet}` }); return interaction.editReply({ embeds: [resultEmbed], components: buildResultButtons() }); } async function runSlotsBack( interaction ) { const footer = interaction.message ?.embeds?.[0] ?.footer ?.text; const parts = footer?.split(":") ?? []; if ( parts[0] !== "crumbSaucerSlotsResult" ) { return temporaryEphemeralReply( interaction, { content: "This Slots result can no longer be replayed." } ); } const ownerId = parts[1]; const bet = Number(parts[2]); if ( ownerId !== interaction.user.id ) { return temporaryEphemeralReply( interaction, { content: "These aren't your slots." } ); } if (!SLOT_BETS.includes(bet)) { return temporaryEphemeralReply( interaction, { content: "The previous wager is no longer available." } ); } return playSlots( interaction, bet, "run_it_back" ); } function buildSlotBetButtons() { return [ new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId( "crumbSaucerSlots10" ) .setLabel("10") .setEmoji("🍪") .setStyle( ButtonStyle.Secondary ), new ButtonBuilder() .setCustomId( "crumbSaucerSlots50" ) .setLabel("50") .setEmoji("🍪") .setStyle( ButtonStyle.Secondary ), new ButtonBuilder() .setCustomId( "crumbSaucerSlots100" ) .setLabel("100") .setEmoji("🍪") .setStyle( ButtonStyle.Primary ), new ButtonBuilder() .setCustomId( "crumbSaucerSlots500" ) .setLabel("500") .setEmoji("🍪") .setStyle( ButtonStyle.Primary ), new ButtonBuilder() .setCustomId( "crumbSaucerSlots1000" ) .setLabel("1,000") .setEmoji("🍪") .setStyle( ButtonStyle.Danger ) ), new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId( "crumbSaucerBack" ) .setLabel( "Back to Crumb Saucer" ) .setEmoji("◀️") .setStyle( ButtonStyle.Secondary ) ) ]; } function buildResultButtons() { return [ new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId( "crumbSaucerSlotsRunItBack" ) .setLabel( "Run It Back" ) .setEmoji("🔁") .setStyle( ButtonStyle.Success ), new ButtonBuilder() .setCustomId( "crumbSaucerSlots" ) .setLabel( "Change Bet" ) .setEmoji("🎰") .setStyle( ButtonStyle.Primary ), new ButtonBuilder() .setCustomId( "crumbSaucerBack" ) .setLabel( "Crumb Saucer" ) .setEmoji("◀️") .setStyle( ButtonStyle.Secondary ) ) ]; } function rollSlotOutcome() { const totalWeight = SLOT_OUTCOMES.reduce( (total, outcome) => total + outcome.weight, 0 ); let roll = Math.random() * totalWeight; for (const outcome of SLOT_OUTCOMES) { roll -= outcome.weight; if (roll <= 0) { return outcome; } } return SLOT_OUTCOMES[0]; } function generateRandomLine() { return [ randomItem(SLOT_SYMBOLS), randomItem(SLOT_SYMBOLS), randomItem(SLOT_SYMBOLS) ]; } function generateOutcomeLine( outcome ) { if ( outcome.type === "triple" ) { return [ outcome.symbol, outcome.symbol, outcome.symbol ]; } if ( outcome.type === "pair" ) { const symbol = randomItem(SLOT_SYMBOLS); let other; do { other = randomItem(SLOT_SYMBOLS); } while (other === symbol); const result = [ symbol, symbol, symbol ]; result[ Math.floor( Math.random() * 3 ) ] = other; return result; } let result; do { result = generateRandomLine(); } while ( new Set(result).size < 3 ); return result; } function buildSpinningEmbed( bet, symbols, status ) { return new EmbedBuilder() .setTitle( "🎰 Crumb Saucer Slots" ) .setDescription( [ "## " + symbols.join(" "), "", `**${status}**` ].join("\n") ) .addFields({ name: "Bet", value: `**${bet.toLocaleString()} 🍪**` }); } function getSlotResultName( outcome ) { if (outcome.type === "loss") { return "No luck this time."; } 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 "Winner!"; } } function getSlotsSessionOwner( interaction ) { const footer = interaction.message ?.embeds?.[0] ?.footer ?.text; const parts = footer?.split(":") ?? []; if ( parts[0] === "crumbSaucer" ) { return parts[1]; } if ( parts[0] === "crumbSaucerSlots" ) { return parts[1]; } if ( parts[0] === "crumbSaucerSlotsResult" ) { return parts[1]; } return null; } function sleep(ms) { return new Promise( resolve => setTimeout( resolve, ms ) ); } module.exports = { showSlots, playSlots, runSlotsBack };