const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js"); const { getNutBalance, transferNuts, temporaryEphemeralReply, randomItem } = require("./nutEconomy"); /* * ============================================================ * CONFIG * ============================================================ */ const SLOT_BETS = [ 10, 50, 100, 500, 1000 ]; /* * ============================================================ * NUT SYMBOLS * ============================================================ * * All symbols are nut-themed. * * 🌰 Chestnut * πŸ₯œ Peanuts * πŸ₯₯ Coconut * 🍩 Doughnut */ const SYMBOLS = [ "🌰", "πŸ₯œ", "πŸ₯₯", "🍩" ]; /* * Weighted reel. * * Chestnuts are the common symbol. * Peanuts are less common. * Coconuts are rare. * Doughnuts are the jackpot symbol. */ const REEL = [ "🌰", "🌰", "🌰", "🌰", "🌰", "πŸ₯œ", "πŸ₯œ", "πŸ₯œ", "πŸ₯œ", "πŸ₯₯", "πŸ₯₯", "🍩" ]; /* * Triple-symbol payouts. * * Payout includes the original wager. */ const PAYOUTS = { "🌰": 1.5, "πŸ₯œ": 2, "πŸ₯₯": 4, "🍩": 10 }; /* * ============================================================ * SHOW SLOTS * ============================================================ */ async function showNutSlots( interaction ) { const ownerId = getNutSlotsOwner( interaction ); if ( ownerId !== interaction.user.id ) { return temporaryEphemeralReply( interaction, { content: "Das ist nicht deine Nut-Hub-Session." } ); } /* * Acknowledge the button immediately. */ await interaction.deferUpdate(); const balance = await getNutBalance( interaction.user.id ); const embed = new EmbedBuilder() .setTitle( "🎰 Nut Slots" ) .setDescription( [ "Dreh die Walzen und jag den **Nut Jackpot**.", "", "### πŸ₯œ Payouts", "🌰 🌰 🌰 β€” **Γ—1.5**", "πŸ₯œ πŸ₯œ πŸ₯œ β€” **Γ—2**", "πŸ₯₯ πŸ₯₯ πŸ₯₯ β€” **Γ—4**", "🍩 🍩 🍩 β€” **Γ—10**", "", "Choose your wager." ].join("\n") ) .addFields({ name: "Deine NΓΌsse", value: `**${balance.toLocaleString()} πŸ₯œ**` }) .setFooter({ text: `nutSlots:${interaction.user.id}` }); return interaction.editReply({ embeds: [ embed ], components: buildSlotBetButtons() }); } /* * ============================================================ * PLAY * ============================================================ */ async function playNutSlots( interaction, bet ) { if ( !SLOT_BETS.includes( bet ) ) { return temporaryEphemeralReply( interaction, { content: "Dieser Einsatz ist nicht verfΓΌgbar." } ); } const ownerId = getNutSlotsOwner( interaction ); if ( ownerId !== interaction.user.id ) { return temporaryEphemeralReply( interaction, { content: "Das ist nicht deine Slots-Session." } ); } const playerId = interaction.user.id; const houseId = interaction.client.user.id; const [ playerBalance, houseBalance ] = await Promise.all([ getNutBalance( playerId ), getNutBalance( houseId ) ]); if ( playerBalance < bet ) { return temporaryEphemeralReply( interaction, { content: `Du brauchst **${bet.toLocaleString()} NΓΌsse** fΓΌr diesen Einsatz.` } ); } /* * Maximum possible return = x10. * * After receiving the wager, the house must * still be able to cover the total payout. */ const maxPayout = bet * 10; if ( houseBalance + bet < maxPayout ) { return temporaryEphemeralReply( interaction, { content: "Der Nut Vault kann diesen Einsatz aktuell nicht absichern." } ); } try { await transferNuts( playerId, houseId, bet ); } catch (error) { if ( error.message === "INSUFFICIENT_NUTS" ) { return temporaryEphemeralReply( interaction, { content: "Dein Nut-Bestand hat sich verΓ€ndert." } ); } throw error; } /* * Initial animation state. */ await interaction.update({ embeds: [ buildSpinEmbed( bet, [ "❔", "❔", "❔" ], "Die Walzen drehen..." ) ], components: [] }); await delay( 500 ); await interaction.editReply({ embeds: [ buildSpinEmbed( bet, [ randomReelSymbol(), "❔", "❔" ], "Die erste Walze stoppt..." ) ] }); await delay( 500 ); const first = randomReelSymbol(); const second = randomReelSymbol(); await interaction.editReply({ embeds: [ buildSpinEmbed( bet, [ first, second, "❔" ], "Noch eine..." ) ] }); await delay( 600 ); /* * Final actual result. * * Generate all three here so animation values * don't alter game odds. */ const result = [ randomReelSymbol(), randomReelSymbol(), randomReelSymbol() ]; const multiplier = getSlotMultiplier( result ); const payout = Math.floor( bet * multiplier ); if ( payout > 0 ) { await transferNuts( houseId, playerId, payout ); } const balanceAfter = await getNutBalance( playerId ); const profit = payout - bet; const embed = new EmbedBuilder() .setTitle( multiplier > 0 ? "🎰 Nut Slots β€” WIN!" : "🎰 Nut Slots" ) .setDescription( [ "## " + result.join( " " ), "", multiplier > 0 ? `πŸŽ‰ **x${multiplier}**` : "Die NΓΌsse bleiben beim Haus." ].join("\n") ) .addFields( { name: "Einsatz", value: `${bet.toLocaleString()} πŸ₯œ`, inline: true }, { name: "Payout", value: `${payout.toLocaleString()} πŸ₯œ`, inline: true }, { name: "Profit", value: `${profit >= 0 ? "+" : ""}${profit.toLocaleString()} πŸ₯œ`, inline: true }, { name: "Deine NΓΌsse", value: `${balanceAfter.toLocaleString()} πŸ₯œ` } ) .setFooter({ text: `nutSlots:${interaction.user.id}` }); return interaction.editReply({ embeds: [ embed ], components: buildSlotResultButtons() }); } /* * ============================================================ * PAYOUT * ============================================================ */ function getSlotMultiplier( result ) { const [ first, second, third ] = result; /* * Triple. */ if ( first === second && second === third ) { return PAYOUTS[first] ?? 0; } /* * Two matching symbols. * * Half the wager is returned. */ if ( first === second || first === third || second === third ) { return 0.5; } /* * Nut Salad: * * Chestnut + Peanut + Coconut * in any order. * * Returns the wager. */ const nutSalad = new Set( result ); if ( nutSalad.size === 3 && nutSalad.has("🌰") && nutSalad.has("πŸ₯œ") && nutSalad.has("πŸ₯₯") ) { return 1; } return 0; } /* * ============================================================ * DISPLAY * ============================================================ */ function buildSpinEmbed( bet, symbols, status ) { return new EmbedBuilder() .setTitle( "🎰 Nut Slots" ) .setDescription( [ "## " + symbols.join( " " ), "", `**${status}**` ].join("\n") ) .addFields({ name: "Einsatz", value: `${bet.toLocaleString()} πŸ₯œ` }); } /* * ============================================================ * BUTTONS * ============================================================ */ function buildSlotBetButtons() { return [ new ActionRowBuilder() .addComponents( ...SLOT_BETS.map( bet => new ButtonBuilder() .setCustomId( `nutSlotsBet${bet}` ) .setLabel( bet.toLocaleString() ) .setEmoji( "πŸ₯œ" ) .setStyle( bet >= 500 ? ButtonStyle.Primary : ButtonStyle.Secondary ) ) ), new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId( "nutHubBack" ) .setLabel( "Nut Hub" ) .setEmoji( "◀️" ) .setStyle( ButtonStyle.Secondary ) ) ]; } function buildSlotResultButtons() { return [ new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId( "nutHubSlots" ) .setLabel( "Play Again" ) .setEmoji( "🎰" ) .setStyle( ButtonStyle.Primary ), new ButtonBuilder() .setCustomId( "nutHubBack" ) .setLabel( "Nut Hub" ) .setEmoji( "◀️" ) .setStyle( ButtonStyle.Secondary ) ) ]; } /* * ============================================================ * SESSION OWNER * ============================================================ */ function getNutSlotsOwner( interaction ) { const footer = interaction.message ?.embeds?.[0] ?.footer ?.text; const parts = footer?.split(":") ?? []; if ( parts[0] === "nutHub" ) { return parts[1]; } if ( parts[0] === "nutSlots" ) { return parts[1]; } return null; } /* * ============================================================ * RANDOM * ============================================================ */ function randomReelSymbol() { return randomItem( REEL ); } function delay( ms ) { return new Promise( resolve => setTimeout( resolve, ms ) ); } module.exports = { showNutSlots, playNutSlots };