From 9e0870c1ed9487a50a41b2722b1a9ff1fbfea577 Mon Sep 17 00:00:00 2001 From: DeSqBlocki Date: Sat, 22 Aug 2026 22:29:16 +0200 Subject: [PATCH] snapshot --- commands/applications/profile.js | 73 +- utils/cookieHeist.js | 335 +----- utils/crumbBlackjack.js | 710 +++---------- utils/crumbRoulette.js | 460 ++------ utils/crumbSlots.js | 454 ++------ utils/profileSystem.js | 1711 +++++++----------------------- 6 files changed, 797 insertions(+), 2946 deletions(-) diff --git a/commands/applications/profile.js b/commands/applications/profile.js index a71f604..4a7153f 100644 --- a/commands/applications/profile.js +++ b/commands/applications/profile.js @@ -1,34 +1,63 @@ -const { SlashCommandBuilder } = require("discord.js"); +const { + SlashCommandBuilder +} = require( + "discord.js" +); const { buildProfileOverview, - buildProfileButtons, - ensureGuildBadges -} = require("../../utils/profileSystem"); + buildProfileButtons +} = require( + "../../utils/profileSystem" +); + module.exports = { - data: new SlashCommandBuilder() - .setName("profile") - .setDescription("View a player profile") - .addUserOption(option => - option - .setName("user") - .setDescription("Player to view") - ), + data: + new SlashCommandBuilder() + .setName( + "profile" + ) + .setDescription( + "View a player profile" + ) + .addUserOption( + option => + option + .setName( + "user" + ) + .setDescription( + "Player to view" + ) + ), - async execute(interaction) { - const user = interaction.options.getUser("user") ?? interaction.user; - await ensureGuildBadges(interaction.guildId); + async execute( + interaction + ) { + const user = + interaction.options.getUser( + "user" + ) + ?? + interaction.user; + + + const embed = + await buildProfileOverview( + interaction.guildId, + user + ); - const embed = await buildProfileOverview( - interaction.guildId, - user - ); return interaction.reply({ - embeds: [embed], - components: buildProfileButtons() + embeds: [ + embed + ], + + components: + buildProfileButtons() }); } -}; +}; \ No newline at end of file diff --git a/utils/cookieHeist.js b/utils/cookieHeist.js index eab62a6..9735229 100644 --- a/utils/cookieHeist.js +++ b/utils/cookieHeist.js @@ -18,83 +18,41 @@ const { trackProfileEvent } = require("./profileSystem"); - const HEIST_COOLDOWN = 60 * 60; - const HEIST_METHODS = { Sneak: { - baseChance: - 0.65, - - minStealPercent: - 0.01, - - maxStealPercent: - 0.03, - - maxStealAmount: - 1500, - - minPenaltyPercent: - 0.01, - - maxPenaltyPercent: - 0.04, - - maxPenaltyAmount: - 750 + baseChance: 0.65, + minStealPercent: 0.01, + maxStealPercent: 0.03, + maxStealAmount: 1500, + minPenaltyPercent: 0.01, + maxPenaltyPercent: 0.04, + maxPenaltyAmount: 750 }, Hack: { - baseChance: - 0.45, - - minStealPercent: - 0.03, - - maxStealPercent: - 0.06, - - maxStealAmount: - 3500, - - minPenaltyPercent: - 0.03, - - maxPenaltyPercent: - 0.07, - - maxPenaltyAmount: - 2000 + baseChance: 0.45, + minStealPercent: 0.03, + maxStealPercent: 0.06, + maxStealAmount: 3500, + minPenaltyPercent: 0.03, + maxPenaltyPercent: 0.07, + maxPenaltyAmount: 2000 }, Smash: { - baseChance: - 0.25, - - minStealPercent: - 0.06, - - maxStealPercent: - 0.12, - - maxStealAmount: - 7500, - - minPenaltyPercent: - 0.06, - - maxPenaltyPercent: - 0.12, - - maxPenaltyAmount: - 5000 + baseChance: 0.25, + minStealPercent: 0.06, + maxStealPercent: 0.12, + maxStealAmount: 7500, + minPenaltyPercent: 0.06, + maxPenaltyPercent: 0.12, + maxPenaltyAmount: 5000 } }; - async function resolveCookieHeist( interaction, methodName @@ -105,20 +63,13 @@ async function resolveCookieHeist( ?.footer ?.text; - const [ type, ownerId - ] = - footer?.split(":") - ?? - []; - + ] = footer?.split(":") ?? []; if ( - type !== - "cookieHeist" - || + type !== "cookieHeist" || ownerId !== interaction.user.id ) { @@ -131,13 +82,11 @@ async function resolveCookieHeist( ); } - const method = HEIST_METHODS[ methodName ]; - if (!method) { return temporaryEphemeralReply( interaction, @@ -148,7 +97,6 @@ async function resolveCookieHeist( ); } - const cooldown = await claimCooldown( interaction.user.id, @@ -157,10 +105,7 @@ async function resolveCookieHeist( HEIST_COOLDOWN ); - - if ( - !cooldown.claimed - ) { + if (!cooldown.claimed) { return temporaryEphemeralReply( interaction, { @@ -171,7 +116,6 @@ async function resolveCookieHeist( ); } - await interaction.update({ embeds: [ new EmbedBuilder() @@ -188,40 +132,26 @@ async function resolveCookieHeist( `cookieHeist:${interaction.user.id}` }) ], - components: [] }); - const playerId = interaction.user.id; - const vaultId = interaction.client.user.id; - const [ playerCookies, vaultCookies - ] = - await Promise.all([ - getCookieBalance( - playerId - ), - - getCookieBalance( - vaultId - ) - ]); - + ] = await Promise.all([ + getCookieBalance(playerId), + getCookieBalance(vaultId) + ]); if ( - playerCookies < - 1 - || - vaultCookies < - 1 + playerCookies < 1 || + vaultCookies < 1 ) { return interaction.editReply({ embeds: [ @@ -235,7 +165,6 @@ async function resolveCookieHeist( .addFields({ name: "Next Heist", - value: `` }) @@ -244,38 +173,29 @@ async function resolveCookieHeist( `cookieHeist:${playerId}` }) ], - components: buildReturnButton() }); } - - await sleep( - 1200 - ); - + await sleep(1200); const securityPenalty = getVaultSecurityPenalty( vaultCookies ); - const successChance = Math.max( 0.10, - method.baseChance - securityPenalty ); - const success = Math.random() < successChance; - if (success) { const rolledPercent = randomFloat( @@ -283,31 +203,25 @@ async function resolveCookieHeist( 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 === @@ -325,7 +239,6 @@ async function resolveCookieHeist( .addFields({ name: "Next Heist", - value: `` }) @@ -334,90 +247,68 @@ async function resolveCookieHeist( `cookieHeist:${playerId}` }) ], - components: buildReturnButton() }); } - throw error; } - await trackProfileEvent( interaction, playerId, { type: "game_result", - game: "heist", - result: "win", - profit: amount } ); - await trackProfileEvent( interaction, playerId, { type: "vault_steal", - amount } ); - await trackProfileEvent( interaction, playerId, { type: "cookie_gain", - amount } ); - const [ playerAfter, vaultAfter - ] = - await Promise.all([ - getCookieBalance( - playerId - ), - - getCookieBalance( - vaultId - ) - ]); - + ] = await Promise.all([ + getCookieBalance(playerId), + getCookieBalance(vaultId) + ]); const actualPercentage = ( amount / vaultCookies - ) - * - 100; - + ) * 100; const embed = new EmbedBuilder() .setTitle( "💥 Vault Breached!" ) - .setDescription( getSuccessText( methodName, @@ -425,134 +316,101 @@ async function resolveCookieHeist( 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 - ], - + embeds: [embed], components: buildReturnButton() }); } - const penaltyPercent = randomFloat( method.minPenaltyPercent, method.maxPenaltyPercent ); - const penalty = Math.max( 1, - Math.min( Math.floor( playerCookies * penaltyPercent ), - method.maxPenaltyAmount, - playerCookies ) ); - try { await transferCookies( playerId, vaultId, penalty ); - } catch (error) { if ( error.message === @@ -570,7 +428,6 @@ async function resolveCookieHeist( .addFields({ name: "Next Heist", - value: `` }) @@ -579,79 +436,59 @@ async function resolveCookieHeist( `cookieHeist:${playerId}` }) ], - components: buildReturnButton() }); } - throw error; } - await trackProfileEvent( interaction, playerId, { type: "game_result", - game: "heist", - result: "loss", - profit: -penalty } ); - await trackProfileEvent( interaction, playerId, { type: "cookie_loss", - amount: penalty } ); - const [ playerAfter, vaultAfter - ] = - await Promise.all([ - getCookieBalance( - playerId - ), - - getCookieBalance( - vaultId - ) - ]); - + ] = await Promise.all([ + getCookieBalance(playerId), + getCookieBalance(vaultId) + ]); const actualPenaltyPercentage = ( penalty / playerCookies - ) - * - 100; - + ) * 100; const embed = new EmbedBuilder() .setTitle( "🚨 HEIST FAILED" ) - .setDescription( getFailureText( methodName, @@ -659,103 +496,76 @@ async function resolveCookieHeist( 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 - ], - + embeds: [embed], components: buildReturnButton() }); } - function buildReturnButton() { return [ new ActionRowBuilder() @@ -767,9 +577,7 @@ function buildReturnButton() { .setLabel( "Back to Crumb Saucer" ) - .setEmoji( - "◀️" - ) + .setEmoji("◀️") .setStyle( ButtonStyle.Secondary ) @@ -777,63 +585,47 @@ function buildReturnButton() { ]; } - function getVaultSecurityPenalty( vaultCookies ) { return Math.min( Math.log10( - vaultCookies + - 1 - ) - * - 0.025, - + vaultCookies + 1 + ) * 0.025, 0.15 ); } - function getHeistProgressText( methodName ) { - switch ( - 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..."; } } - function getSuccessText( methodName, user, @@ -842,40 +634,29 @@ function getSuccessText( const cookies = `**${amount.toLocaleString()} Cookies**`; - - switch ( - methodName - ) { + 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}.` ); } } - function getFailureText( methodName, user, @@ -884,49 +665,36 @@ function getFailureText( const cookies = `**${penalty.toLocaleString()} Cookies**`; - - switch ( - methodName - ) { + 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}.` ); } } - -function sleep( - ms -) { +function sleep(ms) { return new Promise( resolve => setTimeout( @@ -936,8 +704,7 @@ function sleep( ); } - module.exports = { resolveCookieHeist, HEIST_COOLDOWN -}; \ No newline at end of file +}; diff --git a/utils/crumbBlackjack.js b/utils/crumbBlackjack.js index dd3a6d2..979d03d 100644 --- a/utils/crumbBlackjack.js +++ b/utils/crumbBlackjack.js @@ -21,11 +21,9 @@ const { trackProfileEvent } = require("./profileSystem"); - const GAME_COLLECTION = "items_games"; - const BLACKJACK_BETS = [ 10, 50, @@ -34,7 +32,6 @@ const BLACKJACK_BETS = [ 1000 ]; - async function showBlackjack( interaction ) { @@ -43,7 +40,6 @@ async function showBlackjack( interaction ); - if ( ownerId !== interaction.user.id @@ -57,42 +53,32 @@ async function showBlackjack( ); } - const active = await getActiveBlackjack( interaction.user.id, interaction.guildId ); - if (active) { return interaction.update({ embeds: [ - buildGameEmbed( - active - ) + buildGameEmbed(active) ], - components: - buildGameButtons( - active - ) + buildGameButtons(active) }); } - const balance = await getCookieBalance( interaction.user.id ); - const embed = new EmbedBuilder() .setTitle( "🃏 Crumb Saucer Blackjack" ) - .setDescription( [ "Beat the dealer without going over **21**.", @@ -104,44 +90,33 @@ async function showBlackjack( "Choose your wager below." ].join("\n") ) - .addFields({ name: "Your Vault", - value: `**${balance.toLocaleString()} 🍪**` }) - .setFooter({ text: `crumbSaucerBlackjack:${interaction.user.id}` }); - return interaction.update({ - embeds: [ - embed - ], - + embeds: [embed], components: buildBetButtons() }); } - async function startBlackjack( interaction, bet, - allowCustomBet = - false + allowCustomBet = false, + allIn = false ) { if ( - !allowCustomBet - && - !BLACKJACK_BETS.includes( - bet - ) + !allowCustomBet && + !BLACKJACK_BETS.includes(bet) ) { return temporaryEphemeralReply( interaction, @@ -152,14 +127,9 @@ async function startBlackjack( ); } - if ( - !Number.isSafeInteger( - bet - ) - || - bet <= - 0 + !Number.isSafeInteger(bet) || + bet <= 0 ) { return temporaryEphemeralReply( interaction, @@ -170,13 +140,11 @@ async function startBlackjack( ); } - const ownerId = getBlackjackSessionOwner( interaction ); - if ( ownerId !== interaction.user.id @@ -190,26 +158,20 @@ async function startBlackjack( ); } - - const { - db - } = + const { db } = getCollections(); - const games = db.collection( GAME_COLLECTION ); - const existing = await getActiveBlackjack( interaction.user.id, interaction.guildId ); - if (existing) { return temporaryEphemeralReply( interaction, @@ -220,34 +182,21 @@ async function startBlackjack( ); } - const playerId = interaction.user.id; - const houseId = interaction.client.user.id; - const [ playerBalance, houseBalance - ] = - await Promise.all([ - getCookieBalance( - playerId - ), + ] = await Promise.all([ + getCookieBalance(playerId), + getCookieBalance(houseId) + ]); - getCookieBalance( - houseId - ) - ]); - - - if ( - playerBalance < - bet - ) { + if (playerBalance < bet) { return temporaryEphemeralReply( interaction, { @@ -257,13 +206,9 @@ async function startBlackjack( ); } - if ( houseBalance < - Math.ceil( - bet * - 1.5 - ) + Math.ceil(bet * 1.5) ) { return temporaryEphemeralReply( interaction, @@ -274,14 +219,12 @@ async function startBlackjack( ); } - try { await transferCookies( playerId, houseId, bet ); - } catch (error) { if ( error.message === @@ -296,86 +239,57 @@ async function startBlackjack( ); } - throw error; } - const deck = createDeck(); - - shuffleDeck( - deck - ); - + shuffleDeck(deck); const playerCards = [ deck.pop(), deck.pop() ]; - const dealerCards = [ deck.pop(), deck.pop() ]; - const game = { eventType: "Blackjack", - gameId: randomUUID(), - userId: playerId, - guildId: interaction.guildId, - channelId: interaction.channelId, - state: "Playing", - bet, - originalBet: bet, - doubled: false, - + allIn, deck, - playerCards, - dealerCards, - createdAt: getUnixTime() }; - - await games.insertOne( - game - ); - + await games.insertOne(game); const playerBlackjack = - isBlackjack( - playerCards - ); - + isBlackjack(playerCards); const dealerBlackjack = - isBlackjack( - dealerCards - ); - + isBlackjack(dealerCards); if ( playerBlackjack || @@ -392,7 +306,6 @@ async function startBlackjack( ); } - if (playerBlackjack) { return finishBlackjack( interaction, @@ -401,7 +314,6 @@ async function startBlackjack( ); } - return finishBlackjack( interaction, game, @@ -409,22 +321,15 @@ async function startBlackjack( ); } - return interaction.update({ embeds: [ - buildGameEmbed( - game - ) + buildGameEmbed(game) ], - components: - buildGameButtons( - game - ) + buildGameButtons(game) }); } - async function startBlackjackAllIn( interaction ) { @@ -433,7 +338,6 @@ async function startBlackjackAllIn( interaction ); - if ( ownerId !== interaction.user.id @@ -447,17 +351,12 @@ async function startBlackjackAllIn( ); } - const balance = await getCookieBalance( interaction.user.id ); - - if ( - balance <= - 0 - ) { + if (balance <= 0) { return temporaryEphemeralReply( interaction, { @@ -467,15 +366,14 @@ async function startBlackjackAllIn( ); } - return startBlackjack( interaction, balance, + true, true ); } - async function runBlackjackBack( interaction ) { @@ -485,12 +383,8 @@ async function runBlackjackBack( ?.footer ?.text; - const parts = - footer?.split(":") - ?? - []; - + footer?.split(":") ?? []; if ( parts[0] !== @@ -505,16 +399,11 @@ async function runBlackjackBack( ); } - const ownerId = parts[1]; - const bet = - Number( - parts[2] - ); - + Number(parts[2]); if ( ownerId !== @@ -529,14 +418,9 @@ async function runBlackjackBack( ); } - if ( - !Number.isSafeInteger( - bet - ) - || - bet <= - 0 + !Number.isSafeInteger(bet) || + bet <= 0 ) { return temporaryEphemeralReply( interaction, @@ -547,15 +431,14 @@ async function runBlackjackBack( ); } - return startBlackjack( interaction, bet, - true + true, + false ); } - async function hitBlackjack( interaction ) { @@ -564,7 +447,6 @@ async function hitBlackjack( interaction ); - if (!game) { return temporaryEphemeralReply( interaction, @@ -575,7 +457,6 @@ async function hitBlackjack( ); } - if ( game.userId !== interaction.user.id @@ -589,7 +470,6 @@ async function hitBlackjack( ); } - if ( game.state !== "Playing" @@ -603,36 +483,24 @@ async function hitBlackjack( ); } - const deck = - [ - ...game.deck - ]; - + [...game.deck]; const playerCards = - [ - ...game.playerCards - ]; - + [...game.playerCards]; playerCards.push( deck.pop() ); - const score = getHandValue( playerCards ); - - const { - db - } = + const { db } = getCollections(); - await db .collection( GAME_COLLECTION @@ -641,7 +509,6 @@ async function hitBlackjack( { gameId: game.gameId, - state: "Playing" }, @@ -653,18 +520,13 @@ async function hitBlackjack( } ); - const updated = { ...game, deck, playerCards }; - - if ( - score > - 21 - ) { + if (score > 21) { return finishBlackjack( interaction, updated, @@ -672,42 +534,29 @@ async function hitBlackjack( ); } - - if ( - score === - 21 - ) { + if (score === 21) { return standBlackjack( interaction, updated ); } - return interaction.update({ embeds: [ - buildGameEmbed( - updated - ) + buildGameEmbed(updated) ], - components: - buildGameButtons( - updated - ) + buildGameButtons(updated) }); } - async function standBlackjack( interaction, - suppliedGame = - null + suppliedGame = null ) { let game = suppliedGame; - if (!game) { game = await getGameFromInteraction( @@ -715,7 +564,6 @@ async function standBlackjack( ); } - if (!game) { return temporaryEphemeralReply( interaction, @@ -726,7 +574,6 @@ async function standBlackjack( ); } - if ( game.userId !== interaction.user.id @@ -740,24 +587,14 @@ async function standBlackjack( ); } - const deck = - [ - ...game.deck - ]; - + [...game.deck]; const dealerCards = - [ - ...game.dealerCards - ]; - + [...game.dealerCards]; while ( - getHandValue( - dealerCards - ) - < + getHandValue(dealerCards) < 17 ) { dealerCards.push( @@ -765,62 +602,46 @@ async function standBlackjack( ); } - const updated = { ...game, deck, dealerCards }; - const playerScore = getHandValue( updated.playerCards ); - const dealerScore = getHandValue( dealerCards ); - let result; - - if ( - dealerScore > - 21 - ) { - result = - "DealerBust"; + if (dealerScore > 21) { + result = "DealerBust"; } - else if ( playerScore > dealerScore ) { - result = - "Win"; + result = "Win"; } - else if ( playerScore < dealerScore ) { - result = - "Loss"; + result = "Loss"; } - else { - result = - "Push"; + result = "Push"; } - return finishBlackjack( interaction, updated, @@ -828,7 +649,6 @@ async function standBlackjack( ); } - async function doubleBlackjack( interaction ) { @@ -837,7 +657,6 @@ async function doubleBlackjack( interaction ); - if (!game) { return temporaryEphemeralReply( interaction, @@ -848,7 +667,6 @@ async function doubleBlackjack( ); } - if ( game.userId !== interaction.user.id @@ -862,11 +680,8 @@ async function doubleBlackjack( ); } - if ( - game.playerCards.length !== - 2 - || + game.playerCards.length !== 2 || game.doubled ) { return temporaryEphemeralReply( @@ -878,13 +693,11 @@ async function doubleBlackjack( ); } - const playerBalance = await getCookieBalance( interaction.user.id ); - if ( playerBalance < game.originalBet @@ -898,14 +711,12 @@ async function doubleBlackjack( ); } - try { await transferCookies( interaction.user.id, interaction.client.user.id, game.originalBet ); - } catch (error) { if ( error.message === @@ -920,49 +731,33 @@ async function doubleBlackjack( ); } - throw error; } - const deck = - [ - ...game.deck - ]; - + [...game.deck]; const playerCards = - [ - ...game.playerCards - ]; - + [...game.playerCards]; playerCards.push( deck.pop() ); - const updated = { ...game, - deck, playerCards, - bet: game.bet + game.originalBet, - doubled: true }; - - const { - db - } = + const { db } = getCollections(); - await db .collection( GAME_COLLECTION @@ -971,7 +766,6 @@ async function doubleBlackjack( { gameId: game.gameId, - state: "Playing" }, @@ -979,22 +773,16 @@ async function doubleBlackjack( $set: { deck, playerCards, - bet: updated.bet, - doubled: true } } ); - if ( - getHandValue( - playerCards - ) - > + getHandValue(playerCards) > 21 ) { return finishBlackjack( @@ -1004,14 +792,12 @@ async function doubleBlackjack( ); } - return standBlackjack( interaction, updated ); } - async function finishBlackjack( interaction, game, @@ -1020,60 +806,37 @@ async function finishBlackjack( const houseId = interaction.client.user.id; + let payout = 0; - let payout = - 0; - - - switch ( - result - ) { + switch (result) { case "Win": case "DealerBust": - payout = - game.bet * - 2; - + game.bet * 2; break; - case "Push": - payout = game.bet; - break; - case "Blackjack": - payout = Math.floor( - game.bet * - 2.5 + game.bet * 2.5 ); - break; - default: - - payout = - 0; + payout = 0; } - - if ( - payout > - 0 - ) { + if (payout > 0) { const houseBalance = await getCookieBalance( houseId ); - if ( houseBalance < payout @@ -1082,30 +845,25 @@ async function finishBlackjack( `[Blackjack] House cannot cover payout ${payout} for ${game.userId}` ); - return interaction.update({ embeds: [ new EmbedBuilder() .setTitle( "🃏 Blackjack Error" ) - .setDescription( "The Crumb Saucer vault couldn't cover the payout. Please contact an administrator." ) - .setFooter({ text: `crumbBlackjackResult:${game.userId}:${game.originalBet}` }) ], - components: buildResultButtons() }); } - await transferCookies( houseId, game.userId, @@ -1113,13 +871,9 @@ async function finishBlackjack( ); } - - const { - db - } = + const { db } = getCollections(); - await db .collection( GAME_COLLECTION @@ -1128,7 +882,6 @@ async function finishBlackjack( { gameId: game.gameId, - state: "Playing" }, @@ -1136,123 +889,92 @@ async function finishBlackjack( $set: { state: "Finished", - result, - payout, - finishedAt: getUnixTime(), - dealerCards: game.dealerCards, - playerCards: game.playerCards } } ); - const playerBalance = await getCookieBalance( game.userId ); - const profit = - payout - - game.bet; - + payout - game.bet; let profileResult; - - if ( - result === - "Push" - ) { - profileResult = - "push"; + if (result === "Push") { + profileResult = "push"; } - else if ( [ "Win", "DealerBust", "Blackjack" - ].includes( - result - ) + ].includes(result) ) { - profileResult = - "win"; + profileResult = "win"; } - else { - profileResult = - "loss"; + profileResult = "loss"; } - await trackProfileEvent( interaction, game.userId, { type: "game_result", - game: "blackjack", - result: profileResult, - - profit + profit, + natural: + result === + "Blackjack", + allIn: + game.allIn === + true } ); - - if ( - profit > - 0 - ) { + if (profit > 0) { await trackProfileEvent( interaction, game.userId, { type: "cookie_gain", - amount: profit } ); } - - else if ( - profit < - 0 - ) { + else if (profit < 0) { await trackProfileEvent( interaction, game.userId, { type: "cookie_loss", - amount: - Math.abs( - profit - ) + Math.abs(profit) } ); } - const description = [ "**Dealer**", formatCards( @@ -1267,99 +989,65 @@ async function finishBlackjack( `Score: **${getHandValue(game.playerCards)}**` ]; - - if ( - result === - "Push" - ) { + if (result === "Push") { description.push( "", "🤝 **Push — your wager has been refunded.**" ); } - const embed = new EmbedBuilder() .setTitle( - getResultTitle( - result - ) + getResultTitle(result) ) - .setDescription( - description.join( - "\n" - ) + description.join("\n") ) - .addFields( { name: "Bet", - value: `${game.bet.toLocaleString()} 🍪`, - inline: true }, - { name: "Payout", - value: `${payout.toLocaleString()} 🍪`, - inline: true }, - { name: "Profit", - value: `${profit >= 0 ? "+" : ""}${profit.toLocaleString()} 🍪`, - inline: true }, - { name: "Your Vault", - value: `${playerBalance.toLocaleString()} 🍪` } ) - - /* - * Important: - * - * Run It Back uses the original wager, - * not the doubled total. - */ - .setFooter({ text: `crumbBlackjackResult:${game.userId}:${game.originalBet}` }); - return interaction.update({ - embeds: [ - embed - ], - + embeds: [embed], components: buildResultButtons() }); } - function buildGameEmbed( game ) { @@ -1368,16 +1056,13 @@ function buildGameEmbed( game.playerCards ); - const dealerVisible = game.dealerCards[0]; - return new EmbedBuilder() .setTitle( "🃏 Crumb Saucer Blackjack" ) - .setDescription( [ "**Dealer**", @@ -1390,38 +1075,30 @@ function buildGameEmbed( `Score: **${playerScore}**` ].join("\n") ) - .addFields( { name: "Bet", - value: `**${game.bet.toLocaleString()} 🍪**`, - inline: true }, - { name: "Status", - value: "**Your turn**", - inline: true } ) - .setFooter({ text: `crumbBlackjack:${game.gameId}:${game.userId}` }); } - function buildBetButtons() { return [ new ActionRowBuilder() @@ -1435,9 +1112,7 @@ function buildBetButtons() { .setLabel( bet.toLocaleString() ) - .setEmoji( - "🍪" - ) + .setEmoji("🍪") .setStyle( bet >= 500 ? ButtonStyle.Primary @@ -1455,9 +1130,7 @@ function buildBetButtons() { .setLabel( "All In" ) - .setEmoji( - "🔥" - ) + .setEmoji("🔥") .setStyle( ButtonStyle.Danger ), @@ -1469,9 +1142,7 @@ function buildBetButtons() { .setLabel( "Back to Crumb Saucer" ) - .setEmoji( - "◀️" - ) + .setEmoji("◀️") .setStyle( ButtonStyle.Secondary ) @@ -1479,17 +1150,13 @@ function buildBetButtons() { ]; } - function buildGameButtons( game ) { const canDouble = - game.playerCards.length === - 2 - && + game.playerCards.length === 2 && !game.doubled; - return [ new ActionRowBuilder() .addComponents( @@ -1497,12 +1164,8 @@ function buildGameButtons( .setCustomId( "crumbBlackjackHit" ) - .setLabel( - "Hit" - ) - .setEmoji( - "➕" - ) + .setLabel("Hit") + .setEmoji("➕") .setStyle( ButtonStyle.Primary ), @@ -1511,12 +1174,8 @@ function buildGameButtons( .setCustomId( "crumbBlackjackStand" ) - .setLabel( - "Stand" - ) - .setEmoji( - "✋" - ) + .setLabel("Stand") + .setEmoji("✋") .setStyle( ButtonStyle.Success ), @@ -1525,12 +1184,8 @@ function buildGameButtons( .setCustomId( "crumbBlackjackDouble" ) - .setLabel( - "Double" - ) - .setEmoji( - "💰" - ) + .setLabel("Double") + .setEmoji("💰") .setDisabled( !canDouble ) @@ -1541,7 +1196,6 @@ function buildGameButtons( ]; } - function buildResultButtons() { return [ new ActionRowBuilder() @@ -1553,9 +1207,7 @@ function buildResultButtons() { .setLabel( "Run It Back" ) - .setEmoji( - "🔁" - ) + .setEmoji("🔁") .setStyle( ButtonStyle.Success ), @@ -1567,9 +1219,7 @@ function buildResultButtons() { .setLabel( "Change Bet" ) - .setEmoji( - "🃏" - ) + .setEmoji("🃏") .setStyle( ButtonStyle.Primary ), @@ -1581,9 +1231,7 @@ function buildResultButtons() { .setLabel( "Crumb Saucer" ) - .setEmoji( - "◀️" - ) + .setEmoji("◀️") .setStyle( ButtonStyle.Secondary ) @@ -1591,7 +1239,6 @@ function buildResultButtons() { ]; } - function createDeck() { const suits = [ "♠️", @@ -1600,35 +1247,16 @@ function createDeck() { "♣️" ]; - const ranks = [ - "A", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "10", - "J", - "Q", - "K" + "A", "2", "3", "4", "5", + "6", "7", "8", "9", "10", + "J", "Q", "K" ]; - const deck = []; - - for ( - const suit - of suits - ) { - for ( - const rank - of ranks - ) { + for (const suit of suits) { + for (const rank of ranks) { deck.push({ rank, suit @@ -1636,32 +1264,24 @@ function createDeck() { } } - return deck; } - function shuffleDeck( deck ) { for ( let i = - deck.length - - 1; - i > - 0; + deck.length - 1; + i > 0; i-- ) { const j = Math.floor( Math.random() * - ( - i + - 1 - ) + (i + 1) ); - [ deck[i], deck[j] @@ -1672,118 +1292,72 @@ function shuffleDeck( } } - function getHandValue( cards ) { - let total = - 0; + let total = 0; + let aces = 0; - - let aces = - 0; - - - for ( - const card - of cards - ) { - switch ( - card.rank - ) { + for (const card of cards) { + switch (card.rank) { case "A": - - total += - 11; - + total += 11; aces++; - break; - case "K": case "Q": case "J": - - total += - 10; - + total += 10; break; - default: - total += - Number( - card.rank - ); + Number(card.rank); } } - while ( - total > - 21 - && - aces > - 0 + total > 21 && + aces > 0 ) { - total -= - 10; - + total -= 10; aces--; } - return total; } - function isBlackjack( cards ) { return ( - cards.length === - 2 - && - getHandValue( - cards - ) === - 21 + cards.length === 2 && + getHandValue(cards) === 21 ); } - function formatCard( card ) { return `**${card.rank}${card.suit}**`; } - function formatCards( cards ) { return cards - .map( - formatCard - ) - .join( - " " - ); + .map(formatCard) + .join(" "); } - async function getActiveBlackjack( userId, guildId ) { - const { - db - } = + const { db } = getCollections(); - return db .collection( GAME_COLLECTION @@ -1791,17 +1365,13 @@ async function getActiveBlackjack( .findOne({ eventType: "Blackjack", - userId, - guildId, - state: "Playing" }); } - async function getGameFromInteraction( interaction ) { @@ -1811,12 +1381,8 @@ async function getGameFromInteraction( ?.footer ?.text; - const parts = - footer?.split(":") - ?? - []; - + footer?.split(":") ?? []; if ( parts[0] !== @@ -1825,17 +1391,12 @@ async function getGameFromInteraction( return null; } - const gameId = parts[1]; - - const { - db - } = + const { db } = getCollections(); - return db .collection( GAME_COLLECTION @@ -1843,15 +1404,12 @@ async function getGameFromInteraction( .findOne({ eventType: "Blackjack", - gameId, - state: "Playing" }); } - function getBlackjackSessionOwner( interaction ) { @@ -1861,12 +1419,8 @@ function getBlackjackSessionOwner( ?.footer ?.text; - const parts = - footer?.split(":") - ?? - []; - + footer?.split(":") ?? []; if ( parts[0] === @@ -1875,7 +1429,6 @@ function getBlackjackSessionOwner( return parts[1]; } - if ( parts[0] === "crumbSaucerBlackjack" @@ -1883,7 +1436,6 @@ function getBlackjackSessionOwner( return parts[1]; } - if ( parts[0] === "crumbBlackjack" @@ -1891,7 +1443,6 @@ function getBlackjackSessionOwner( return parts[2]; } - if ( parts[0] === "crumbBlackjackResult" @@ -1899,63 +1450,42 @@ function getBlackjackSessionOwner( return parts[1]; } - return null; } - function getResultTitle( result ) { - switch ( - result - ) { + switch (result) { case "Blackjack": - return "🃏 BLACKJACK!"; - case "Win": - return "🎉 You Win!"; - case "DealerBust": - return "💥 Dealer Bust!"; - case "Push": - return "🤝 Push — Wager Refunded"; - case "Bust": - return "💥 Bust!"; - case "DealerBlackjack": - return "🃏 Dealer Blackjack"; - default: - return "🃏 Dealer Wins"; } } - module.exports = { showBlackjack, - startBlackjack, startBlackjackAllIn, - hitBlackjack, standBlackjack, doubleBlackjack, - runBlackjackBack -}; \ No newline at end of file +}; diff --git a/utils/crumbRoulette.js b/utils/crumbRoulette.js index 3f93bd3..0073afb 100644 --- a/utils/crumbRoulette.js +++ b/utils/crumbRoulette.js @@ -15,7 +15,6 @@ const { trackProfileEvent } = require("./profileSystem"); - const ROULETTE_BETS = [ 10, 50, @@ -24,32 +23,27 @@ const ROULETTE_BETS = [ 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: "🟢", @@ -57,30 +51,14 @@ const ROULETTE_CHOICES = { } }; - const RED_NUMBERS = new Set([ - 1, - 3, - 5, - 7, - 9, - 12, - 14, - 16, - 18, - 19, - 21, - 23, - 25, - 27, - 30, - 32, - 34, - 36 + 1, 3, 5, 7, 9, + 12, 14, 16, 18, + 19, 21, 23, 25, + 27, 30, 32, 34, 36 ]); - async function showRoulette( interaction ) { @@ -89,7 +67,6 @@ async function showRoulette( interaction ); - if ( ownerId !== interaction.user.id @@ -103,19 +80,16 @@ async function showRoulette( ); } - const balance = await getCookieBalance( interaction.user.id ); - const embed = new EmbedBuilder() .setTitle( "🎡 Crumb Saucer Roulette" ) - .setDescription( [ "European Roulette — **0 to 36**.", @@ -129,41 +103,29 @@ async function showRoulette( "🟢 **Zero** — x36" ].join("\n") ) - .addFields({ name: "Your Vault", - value: `**${balance.toLocaleString()} 🍪**` }) - .setFooter({ text: `crumbRouletteMenu:${interaction.user.id}` }); - return interaction.update({ - embeds: [ - embed - ], - + embeds: [embed], components: buildBetButtons() }); } - async function chooseRouletteBet( interaction, bet ) { - if ( - !ROULETTE_BETS.includes( - bet - ) - ) { + if (!ROULETTE_BETS.includes(bet)) { return temporaryEphemeralReply( interaction, { @@ -173,13 +135,11 @@ async function chooseRouletteBet( ); } - const ownerId = getRouletteSessionOwner( interaction ); - if ( ownerId !== interaction.user.id @@ -193,14 +153,13 @@ async function chooseRouletteBet( ); } - return showRouletteChoices( interaction, - bet + bet, + false ); } - async function startRouletteAllIn( interaction ) { @@ -209,7 +168,6 @@ async function startRouletteAllIn( interaction ); - if ( ownerId !== interaction.user.id @@ -223,17 +181,12 @@ async function startRouletteAllIn( ); } - const balance = await getCookieBalance( interaction.user.id ); - - if ( - balance <= - 0 - ) { + if (balance <= 0) { return temporaryEphemeralReply( interaction, { @@ -243,25 +196,21 @@ async function startRouletteAllIn( ); } - return showRouletteChoices( interaction, - balance + balance, + true ); } - async function showRouletteChoices( interaction, - bet + bet, + allIn = false ) { if ( - !Number.isSafeInteger( - bet - ) - || - bet <= - 0 + !Number.isSafeInteger(bet) || + bet <= 0 ) { return temporaryEphemeralReply( interaction, @@ -272,17 +221,12 @@ async function showRouletteChoices( ); } - const balance = await getCookieBalance( interaction.user.id ); - - if ( - balance < - bet - ) { + if (balance < bet) { return temporaryEphemeralReply( interaction, { @@ -292,13 +236,11 @@ async function showRouletteChoices( ); } - const embed = new EmbedBuilder() .setTitle( "🎡 Crumb Saucer Roulette" ) - .setDescription( [ `Wager: **${bet.toLocaleString()} 🍪**`, @@ -312,32 +254,24 @@ async function showRouletteChoices( "🟢 **Zero** — x36" ].join("\n") ) - .addFields({ name: "Your Vault", - value: `**${balance.toLocaleString()} 🍪**` }) - .setFooter({ text: - `crumbRouletteBet:${interaction.user.id}:${bet}` + `crumbRouletteBet:${interaction.user.id}:${bet}:${allIn ? 1 : 0}` }); - return interaction.update({ - embeds: [ - embed - ], - + embeds: [embed], components: buildChoiceButtons() }); } - async function runRouletteBack( interaction ) { @@ -347,12 +281,8 @@ async function runRouletteBack( ?.footer ?.text; - const parts = - footer?.split(":") - ?? - []; - + footer?.split(":") ?? []; if ( parts[0] !== @@ -367,16 +297,11 @@ async function runRouletteBack( ); } - const ownerId = parts[1]; - const bet = - Number( - parts[2] - ); - + Number(parts[2]); if ( ownerId !== @@ -391,14 +316,9 @@ async function runRouletteBack( ); } - if ( - !Number.isSafeInteger( - bet - ) - || - bet <= - 0 + !Number.isSafeInteger(bet) || + bet <= 0 ) { return temporaryEphemeralReply( interaction, @@ -409,14 +329,13 @@ async function runRouletteBack( ); } - return showRouletteChoices( interaction, - bet + bet, + false ); } - async function playRoulette( interaction, choiceKey @@ -426,7 +345,6 @@ async function playRoulette( choiceKey ]; - if (!choice) { return temporaryEphemeralReply( interaction, @@ -437,19 +355,14 @@ async function playRoulette( ); } - const footer = interaction.message ?.embeds?.[0] ?.footer ?.text; - const parts = - footer?.split(":") - ?? - []; - + footer?.split(":") ?? []; if ( parts[0] !== @@ -464,16 +377,14 @@ async function playRoulette( ); } - const ownerId = parts[1]; - const bet = - Number( - parts[2] - ); + Number(parts[2]); + const allIn = + parts[3] === "1"; if ( ownerId !== @@ -488,14 +399,9 @@ async function playRoulette( ); } - if ( - !Number.isSafeInteger( - bet - ) - || - bet <= - 0 + !Number.isSafeInteger(bet) || + bet <= 0 ) { return temporaryEphemeralReply( interaction, @@ -506,34 +412,21 @@ async function playRoulette( ); } - const playerId = interaction.user.id; - const houseId = interaction.client.user.id; - const [ playerBalance, houseBalance - ] = - await Promise.all([ - getCookieBalance( - playerId - ), + ] = await Promise.all([ + getCookieBalance(playerId), + getCookieBalance(houseId) + ]); - getCookieBalance( - houseId - ) - ]); - - - if ( - playerBalance < - bet - ) { + if (playerBalance < bet) { return temporaryEphemeralReply( interaction, { @@ -543,15 +436,12 @@ async function playRoulette( ); } - const maximumPayout = bet * choice.multiplier; - if ( - houseBalance + - bet < + houseBalance + bet < maximumPayout ) { return temporaryEphemeralReply( @@ -563,14 +453,12 @@ async function playRoulette( ); } - try { await transferCookies( playerId, houseId, bet ); - } catch (error) { if ( error.message === @@ -585,11 +473,9 @@ async function playRoulette( ); } - throw error; } - await interaction.update({ embeds: [ buildSpinEmbed( @@ -599,15 +485,10 @@ async function playRoulette( "The wheel begins to spin..." ) ], - components: [] }); - - await sleep( - 650 - ); - + await sleep(650); await interaction.editReply({ embeds: [ @@ -618,15 +499,10 @@ async function playRoulette( "Round and round..." ) ], - components: [] }); - - await sleep( - 650 - ); - + await sleep(650); await interaction.editReply({ embeds: [ @@ -637,95 +513,69 @@ async function playRoulette( "The ball is slowing down..." ) ], - components: [] }); - - await sleep( - 750 - ); - + await sleep(750); const result = randomRouletteNumber(); - const won = didRouletteBetWin( choiceKey, result ); - const payout = won ? bet * choice.multiplier : 0; - - if ( - payout > - 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 - ) - ]); - + ] = await Promise.all([ + getCookieBalance(playerId), + getCookieBalance(houseId) + ]); const profit = - payout - - bet; - + payout - bet; await trackProfileEvent( interaction, @@ -733,60 +583,51 @@ async function playRoulette( { type: "game_result", - game: "roulette", - result: profit > 0 ? "win" : profit < 0 ? "loss" : "push", - - profit + profit, + zero: + won && + choiceKey === + "Zero", + allIn: + won && + allIn } ); - - if ( - profit > - 0 - ) { + if (profit > 0) { await trackProfileEvent( interaction, playerId, { type: "cookie_gain", - amount: profit } ); } - - else if ( - profit < - 0 - ) { + else if (profit < 0) { await trackProfileEvent( interaction, playerId, { type: "cookie_loss", - amount: - Math.abs( - profit - ) + Math.abs(profit) } ); } - const resultEmbed = new EmbedBuilder() .setTitle( @@ -794,7 +635,6 @@ async function playRoulette( ? "🎉 Roulette Winner!" : "🎡 Roulette" ) - .setDescription( [ "## The ball lands on...", @@ -808,151 +648,97 @@ async function playRoulette( : "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 - ], - + embeds: [resultEmbed], components: buildResultButtons() }); } - function didRouletteBetWin( choice, number ) { - switch ( - choice - ) { + switch (choice) { case "Red": - return ( - number !== - 0 - && - RED_NUMBERS.has( - number - ) + number !== 0 && + RED_NUMBERS.has(number) ); - case "Black": - return ( - number !== - 0 - && - !RED_NUMBERS.has( - number - ) + number !== 0 && + !RED_NUMBERS.has(number) ); - case "Odd": - return ( - number !== - 0 - && - number % - 2 === - 1 + number !== 0 && + number % 2 === 1 ); - case "Even": - return ( - number !== - 0 - && - number % - 2 === - 0 + number !== 0 && + number % 2 === 0 ); - case "Zero": - - return ( - number === - 0 - ); - + return number === 0; default: - return false; } } - function buildSpinEmbed( choice, bet, @@ -963,7 +749,6 @@ function buildSpinEmbed( .setTitle( "🎡 Crumb Saucer Roulette" ) - .setDescription( [ "## 🎡 Spinning...", @@ -973,57 +758,40 @@ function buildSpinEmbed( `**${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 - ) { + if (number === 0) { return "🟢 0"; } - - if ( - RED_NUMBERS.has( - number - ) - ) { + if (RED_NUMBERS.has(number)) { return `🔴 ${number}`; } - return `⚫ ${number}`; } - function buildBetButtons() { return [ new ActionRowBuilder() @@ -1037,9 +805,7 @@ function buildBetButtons() { .setLabel( bet.toLocaleString() ) - .setEmoji( - "🍪" - ) + .setEmoji("🍪") .setStyle( bet >= 500 ? ButtonStyle.Primary @@ -1057,9 +823,7 @@ function buildBetButtons() { .setLabel( "All In" ) - .setEmoji( - "🔥" - ) + .setEmoji("🔥") .setStyle( ButtonStyle.Danger ), @@ -1071,9 +835,7 @@ function buildBetButtons() { .setLabel( "Back to Crumb Saucer" ) - .setEmoji( - "◀️" - ) + .setEmoji("◀️") .setStyle( ButtonStyle.Secondary ) @@ -1081,7 +843,6 @@ function buildBetButtons() { ]; } - function buildChoiceButtons() { return [ new ActionRowBuilder() @@ -1090,12 +851,8 @@ function buildChoiceButtons() { .setCustomId( "crumbRouletteRed" ) - .setLabel( - "Red" - ) - .setEmoji( - "🔴" - ) + .setLabel("Red") + .setEmoji("🔴") .setStyle( ButtonStyle.Danger ), @@ -1104,12 +861,8 @@ function buildChoiceButtons() { .setCustomId( "crumbRouletteBlack" ) - .setLabel( - "Black" - ) - .setEmoji( - "⚫" - ) + .setLabel("Black") + .setEmoji("⚫") .setStyle( ButtonStyle.Secondary ), @@ -1118,9 +871,7 @@ function buildChoiceButtons() { .setCustomId( "crumbRouletteOdd" ) - .setLabel( - "Odd" - ) + .setLabel("Odd") .setStyle( ButtonStyle.Primary ), @@ -1129,9 +880,7 @@ function buildChoiceButtons() { .setCustomId( "crumbRouletteEven" ) - .setLabel( - "Even" - ) + .setLabel("Even") .setStyle( ButtonStyle.Primary ), @@ -1140,12 +889,8 @@ function buildChoiceButtons() { .setCustomId( "crumbRouletteZero" ) - .setLabel( - "Zero" - ) - .setEmoji( - "🟢" - ) + .setLabel("Zero") + .setEmoji("🟢") .setStyle( ButtonStyle.Success ) @@ -1160,9 +905,7 @@ function buildChoiceButtons() { .setLabel( "Change Wager" ) - .setEmoji( - "↩️" - ) + .setEmoji("↩️") .setStyle( ButtonStyle.Secondary ) @@ -1170,7 +913,6 @@ function buildChoiceButtons() { ]; } - function buildResultButtons() { return [ new ActionRowBuilder() @@ -1182,9 +924,7 @@ function buildResultButtons() { .setLabel( "Run It Back" ) - .setEmoji( - "🔁" - ) + .setEmoji("🔁") .setStyle( ButtonStyle.Success ), @@ -1196,9 +936,7 @@ function buildResultButtons() { .setLabel( "Change Bet" ) - .setEmoji( - "🎡" - ) + .setEmoji("🎡") .setStyle( ButtonStyle.Primary ), @@ -1210,9 +948,7 @@ function buildResultButtons() { .setLabel( "Crumb Saucer" ) - .setEmoji( - "◀️" - ) + .setEmoji("◀️") .setStyle( ButtonStyle.Secondary ) @@ -1220,7 +956,6 @@ function buildResultButtons() { ]; } - function getRouletteSessionOwner( interaction ) { @@ -1230,21 +965,13 @@ function getRouletteSessionOwner( ?.footer ?.text; - const parts = - footer?.split(":") - ?? - []; + footer?.split(":") ?? []; - - if ( - parts[0] === - "crumbSaucer" - ) { + if (parts[0] === "crumbSaucer") { return parts[1]; } - if ( parts[0] === "crumbRouletteMenu" @@ -1252,7 +979,6 @@ function getRouletteSessionOwner( return parts[1]; } - if ( parts[0] === "crumbRouletteBet" @@ -1260,7 +986,6 @@ function getRouletteSessionOwner( return parts[1]; } - if ( parts[0] === "crumbRouletteResult" @@ -1268,22 +993,16 @@ function getRouletteSessionOwner( return parts[1]; } - return null; } - function randomRouletteNumber() { return Math.floor( - Math.random() * - 37 + Math.random() * 37 ); } - -function sleep( - ms -) { +function sleep(ms) { return new Promise( resolve => setTimeout( @@ -1293,7 +1012,6 @@ function sleep( ); } - module.exports = { showRoulette, chooseRouletteBet, @@ -1301,4 +1019,4 @@ module.exports = { showRouletteChoices, playRoulette, runRouletteBack -}; \ No newline at end of file +}; diff --git a/utils/crumbSlots.js b/utils/crumbSlots.js index 49e6ee0..7da2072 100644 --- a/utils/crumbSlots.js +++ b/utils/crumbSlots.js @@ -16,7 +16,6 @@ const { trackProfileEvent } = require("./profileSystem"); - const SLOT_BETS = [ 10, 50, @@ -25,7 +24,6 @@ const SLOT_BETS = [ 1000 ]; - const SLOT_SYMBOLS = [ "🍒", "🍋", @@ -34,66 +32,22 @@ 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 - } + { 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 -) { +async function showSlots(interaction) { const ownerId = getSlotsSessionOwner( interaction ); - if ( ownerId !== interaction.user.id @@ -107,19 +61,16 @@ async function showSlots( ); } - const balance = await getCookieBalance( interaction.user.id ); - const embed = new EmbedBuilder() .setTitle( "🎰 Crumb Saucer Slots" ) - .setDescription( [ "Welcome to the Cookie reels.", @@ -133,32 +84,24 @@ async function showSlots( "💎 **Jackpot** — x10" ].join("\n") ) - .addFields({ name: "Your Vault", - value: `**${balance.toLocaleString()} 🍪**` }) - .setFooter({ text: `crumbSaucerSlots:${interaction.user.id}` }); - return interaction.update({ - embeds: [ - embed - ], - + embeds: [embed], components: buildSlotBetButtons() }); } - async function playSlots( interaction, bet @@ -168,7 +111,6 @@ async function playSlots( interaction ); - if ( ownerId !== interaction.user.id @@ -182,12 +124,7 @@ async function playSlots( ); } - - if ( - !SLOT_BETS.includes( - bet - ) - ) { + if (!SLOT_BETS.includes(bet)) { return temporaryEphemeralReply( interaction, { @@ -197,34 +134,21 @@ async function playSlots( ); } - const playerId = interaction.user.id; - const vaultId = interaction.client.user.id; - const [ playerBalance, vaultBalance - ] = - await Promise.all([ - getCookieBalance( - playerId - ), + ] = await Promise.all([ + getCookieBalance(playerId), + getCookieBalance(vaultId) + ]); - getCookieBalance( - vaultId - ) - ]); - - - if ( - playerBalance < - bet - ) { + if (playerBalance < bet) { return temporaryEphemeralReply( interaction, { @@ -234,15 +158,11 @@ async function playSlots( ); } - const maxPayout = - bet * - 10; - + bet * 10; if ( - vaultBalance + - bet < + vaultBalance + bet < maxPayout ) { return temporaryEphemeralReply( @@ -254,14 +174,12 @@ async function playSlots( ); } - try { await transferCookies( playerId, vaultId, bet ); - } catch (error) { if ( error.message === @@ -276,38 +194,24 @@ async function playSlots( ); } - throw error; } - await interaction.update({ embeds: [ buildSpinningEmbed( bet, - [ - "❔", - "❔", - "❔" - ], + ["❔", "❔", "❔"], "The reels are spinning..." ) ], - components: [] }); - - await sleep( - 500 - ); - + await sleep(500); const firstPreview = - randomItem( - SLOT_SYMBOLS - ); - + randomItem(SLOT_SYMBOLS); await interaction.editReply({ embeds: [ @@ -321,21 +225,13 @@ async function playSlots( "The first reel stops..." ) ], - components: [] }); - - await sleep( - 500 - ); - + await sleep(500); const secondPreview = - randomItem( - SLOT_SYMBOLS - ); - + randomItem(SLOT_SYMBOLS); await interaction.editReply({ embeds: [ @@ -349,94 +245,68 @@ async function playSlots( "One more..." ) ], - components: [] }); - - await sleep( - 600 - ); - + await sleep(600); const outcome = rollSlotOutcome(); - const result = generateOutcomeLine( outcome ); - const payout = Math.floor( bet * outcome.multiplier ); - - if ( - payout > - 0 - ) { + 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 - ) - ]); - + ] = await Promise.all([ + getCookieBalance(playerId), + getCookieBalance(vaultId) + ]); const profit = - payout - - bet; - + payout - bet; await trackProfileEvent( interaction, @@ -444,162 +314,124 @@ async function playSlots( { type: "game_result", - game: "slots", - result: profit > 0 ? "win" : profit < 0 ? "loss" : "push", - - profit + profit, + jackpot: + outcome.type === + "triple" && + outcome.symbol === + "💎" } ); - - if ( - profit > - 0 - ) { + if (profit > 0) { await trackProfileEvent( interaction, playerId, { type: "cookie_gain", - amount: profit } ); } - - else if ( - profit < - 0 - ) { + else if (profit < 0) { await trackProfileEvent( interaction, playerId, { type: "cookie_loss", - amount: - Math.abs( - profit - ) + Math.abs(profit) } ); } - const resultEmbed = new EmbedBuilder() .setTitle( "🎰 Crumb Saucer Slots" ) - .setDescription( [ "## " + - result.join( - " " - ), + 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 - ], - + embeds: [resultEmbed], components: buildResultButtons() }); } - async function runSlotsBack( interaction ) { @@ -609,12 +441,8 @@ async function runSlotsBack( ?.footer ?.text; - const parts = - footer?.split(":") - ?? - []; - + footer?.split(":") ?? []; if ( parts[0] !== @@ -629,16 +457,11 @@ async function runSlotsBack( ); } - const ownerId = parts[1]; - const bet = - Number( - parts[2] - ); - + Number(parts[2]); if ( ownerId !== @@ -653,12 +476,7 @@ async function runSlotsBack( ); } - - if ( - !SLOT_BETS.includes( - bet - ) - ) { + if (!SLOT_BETS.includes(bet)) { return temporaryEphemeralReply( interaction, { @@ -668,14 +486,12 @@ async function runSlotsBack( ); } - return playSlots( interaction, bet ); } - function buildSlotBetButtons() { return [ new ActionRowBuilder() @@ -684,68 +500,44 @@ function buildSlotBetButtons() { .setCustomId( "crumbSaucerSlots10" ) - .setLabel( - "10" - ) - .setEmoji( - "🍪" - ) + .setLabel("10") + .setEmoji("🍪") .setStyle( ButtonStyle.Secondary ), - new ButtonBuilder() .setCustomId( "crumbSaucerSlots50" ) - .setLabel( - "50" - ) - .setEmoji( - "🍪" - ) + .setLabel("50") + .setEmoji("🍪") .setStyle( ButtonStyle.Secondary ), - new ButtonBuilder() .setCustomId( "crumbSaucerSlots100" ) - .setLabel( - "100" - ) - .setEmoji( - "🍪" - ) + .setLabel("100") + .setEmoji("🍪") .setStyle( ButtonStyle.Primary ), - new ButtonBuilder() .setCustomId( "crumbSaucerSlots500" ) - .setLabel( - "500" - ) - .setEmoji( - "🍪" - ) + .setLabel("500") + .setEmoji("🍪") .setStyle( ButtonStyle.Primary ), - new ButtonBuilder() .setCustomId( "crumbSaucerSlots1000" ) - .setLabel( - "1,000" - ) - .setEmoji( - "🍪" - ) + .setLabel("1,000") + .setEmoji("🍪") .setStyle( ButtonStyle.Danger ) @@ -760,9 +552,7 @@ function buildSlotBetButtons() { .setLabel( "Back to Crumb Saucer" ) - .setEmoji( - "◀️" - ) + .setEmoji("◀️") .setStyle( ButtonStyle.Secondary ) @@ -770,7 +560,6 @@ function buildSlotBetButtons() { ]; } - function buildResultButtons() { return [ new ActionRowBuilder() @@ -782,9 +571,7 @@ function buildResultButtons() { .setLabel( "Run It Back" ) - .setEmoji( - "🔁" - ) + .setEmoji("🔁") .setStyle( ButtonStyle.Success ), @@ -796,9 +583,7 @@ function buildResultButtons() { .setLabel( "Change Bet" ) - .setEmoji( - "🎰" - ) + .setEmoji("🎰") .setStyle( ButtonStyle.Primary ), @@ -810,9 +595,7 @@ function buildResultButtons() { .setLabel( "Crumb Saucer" ) - .setEmoji( - "◀️" - ) + .setEmoji("◀️") .setStyle( ButtonStyle.Secondary ) @@ -820,64 +603,38 @@ function buildResultButtons() { ]; } - function rollSlotOutcome() { const totalWeight = SLOT_OUTCOMES.reduce( - ( - total, - outcome - ) => + (total, outcome) => total + outcome.weight, - 0 ); - let roll = Math.random() * totalWeight; + for (const outcome of SLOT_OUTCOMES) { + roll -= outcome.weight; - for ( - const outcome - of SLOT_OUTCOMES - ) { - roll -= - outcome.weight; - - - if ( - roll <= - 0 - ) { + if (roll <= 0) { return outcome; } } - return SLOT_OUTCOMES[0]; } - function generateRandomLine() { return [ - randomItem( - SLOT_SYMBOLS - ), - - randomItem( - SLOT_SYMBOLS - ), - - randomItem( - SLOT_SYMBOLS - ) + randomItem(SLOT_SYMBOLS), + randomItem(SLOT_SYMBOLS), + randomItem(SLOT_SYMBOLS) ]; } - function generateOutcomeLine( outcome ) { @@ -892,31 +649,19 @@ function generateOutcomeLine( ]; } - if ( outcome.type === "pair" ) { const symbol = - randomItem( - SLOT_SYMBOLS - ); - + randomItem(SLOT_SYMBOLS); let other; - do { other = - randomItem( - SLOT_SYMBOLS - ); - - } while ( - other === - symbol - ); - + randomItem(SLOT_SYMBOLS); + } while (other === symbol); const result = [ symbol, @@ -924,39 +669,27 @@ function generateOutcomeLine( symbol ]; - result[ Math.floor( - Math.random() * - 3 + Math.random() * 3 ) - ] = - other; - + ] = other; return result; } - let result; - do { result = generateRandomLine(); - } while ( - new Set( - result - ).size < - 3 + new Set(result).size < 3 ); - return result; } - function buildSpinningEmbed( bet, symbols, @@ -966,71 +699,49 @@ function buildSpinningEmbed( .setTitle( "🎰 Crumb Saucer Slots" ) - .setDescription( [ "## " + - symbols.join( - " " - ), + symbols.join(" "), "", `**${status}**` ].join("\n") ) - .addFields({ name: "Bet", - value: `**${bet.toLocaleString()} 🍪**` }); } - function getSlotResultName( outcome ) { - if ( - outcome.type === - "loss" - ) { + if (outcome.type === "loss") { return "No luck this time."; } - - if ( - outcome.type === - "pair" - ) { + if (outcome.type === "pair") { return "Pair!"; } - - switch ( - outcome.symbol - ) { + 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 ) { @@ -1040,12 +751,8 @@ function getSlotsSessionOwner( ?.footer ?.text; - const parts = - footer?.split(":") - ?? - []; - + footer?.split(":") ?? []; if ( parts[0] === @@ -1054,7 +761,6 @@ function getSlotsSessionOwner( return parts[1]; } - if ( parts[0] === "crumbSaucerSlots" @@ -1062,7 +768,6 @@ function getSlotsSessionOwner( return parts[1]; } - if ( parts[0] === "crumbSaucerSlotsResult" @@ -1070,14 +775,10 @@ function getSlotsSessionOwner( return parts[1]; } - return null; } - -function sleep( - ms -) { +function sleep(ms) { return new Promise( resolve => setTimeout( @@ -1087,9 +788,8 @@ function sleep( ); } - module.exports = { showSlots, playSlots, runSlotsBack -}; \ No newline at end of file +}; diff --git a/utils/profileSystem.js b/utils/profileSystem.js index a9b43e3..b371198 100644 --- a/utils/profileSystem.js +++ b/utils/profileSystem.js @@ -6,473 +6,205 @@ const { MessageFlags } = require("discord.js"); -const { - mClient -} = require("../index"); - -const { - getCookieBalance, - getUnixTime -} = require("./cookieEconomy"); - - -/* - * ============================================================ - * CONFIG - * ============================================================ - */ - -const DATABASE_NAME = - process.env.M_DB; +const { mClient } = require("../index"); +const { getCookieBalance, getUnixTime } = require("./cookieEconomy"); +const DATABASE_NAME = process.env.M_DB; const COLLECTIONS = { - stats: - "stats_profiles", - - achievements: - "items_achievements", - - badges: - "guild_badges", - - achievementConfig: - "config_achievements" + stats: "stats_profiles", + achievements: "items_achievements", + badges: "guild_badges", + achievementConfig: "config_achievements" }; - -/* - * ============================================================ - * COLLECTION ACCESS - * ============================================================ - */ - function getProfileCollections() { if (!DATABASE_NAME) { - throw new Error( - "M_DB is not configured in .env" - ); + throw new Error("M_DB is not configured in .env"); } - - const db = - mClient.db( - DATABASE_NAME - ); - + const db = mClient.db(DATABASE_NAME); return { db, - - stats: - db.collection( - COLLECTIONS.stats - ), - - achievements: - db.collection( - COLLECTIONS.achievements - ), - - badges: - db.collection( - COLLECTIONS.badges - ), - - achievementConfig: - db.collection( - COLLECTIONS.achievementConfig - ) + stats: db.collection(COLLECTIONS.stats), + achievements: db.collection(COLLECTIONS.achievements), + badges: db.collection(COLLECTIONS.badges), + achievementConfig: db.collection(COLLECTIONS.achievementConfig) }; } - -/* - * ============================================================ - * INITIALIZE - * ============================================================ - */ - async function ensureProfileSystem() { const { stats, achievements, badges, achievementConfig - } = - getProfileCollections(); - + } = getProfileCollections(); await stats.createIndex( - { - userId: - 1, - - guildId: - 1 - }, - { - unique: - true - } + { userId: 1, guildId: 1 }, + { unique: true } ); - await achievements.createIndex( - { - userId: - 1, - - guildId: - 1 - }, - { - unique: - true - } + { userId: 1, guildId: 1 }, + { unique: true } ); - await badges.createIndex( - { - guildId: - 1, - - badgeId: - 1 - }, - { - unique: - true - } + { guildId: 1, badgeId: 1 }, + { unique: true } ); - await achievementConfig.createIndex( - { - achievementId: - 1 - }, - { - unique: - true - } + { achievementId: 1 }, + { unique: true } ); } - -/* - * ============================================================ - * DEFAULT STATS - * ============================================================ - */ - -function getDefaultStats( - userId, - guildId -) { +function getDefaultStats(userId, guildId) { return { userId, guildId, economy: { - cookiesObtainedTotal: - 0, - - cookiesLostTotal: - 0, - - cookiesGivenTotal: - 0, - - stolenTotal: - 0, - - stolenFromMeTotal: - 0, - - vaultStolenTotal: - 0 + cookiesObtainedTotal: 0, + cookiesLostTotal: 0, + cookiesGivenTotal: 0, + stolenTotal: 0, + stolenFromMeTotal: 0, + vaultStolenTotal: 0 }, unique: { - playersStolenFrom: - [], - - playersStolenBy: - [] + playersStolenFrom: [], + playersStolenBy: [] }, games: { total: { - played: - 0, - - won: - 0, - - lost: - 0, - - pushed: - 0 + played: 0, + won: 0, + lost: 0, + pushed: 0 }, slots: { - played: - 0, - - won: - 0, - - lost: - 0, - - pushed: - 0, - - cookiesWon: - 0, - - cookiesLost: - 0 + played: 0, + won: 0, + lost: 0, + pushed: 0, + cookiesWon: 0, + cookiesLost: 0 }, blackjack: { - played: - 0, - - won: - 0, - - lost: - 0, - - pushed: - 0, - - cookiesWon: - 0, - - cookiesLost: - 0 + played: 0, + won: 0, + lost: 0, + pushed: 0, + cookiesWon: 0, + cookiesLost: 0 }, roulette: { - played: - 0, - - won: - 0, - - lost: - 0, - - pushed: - 0, - - cookiesWon: - 0, - - cookiesLost: - 0 + played: 0, + won: 0, + lost: 0, + pushed: 0, + cookiesWon: 0, + cookiesLost: 0 }, heist: { - played: - 0, - - won: - 0, - - lost: - 0, - - pushed: - 0, - - cookiesWon: - 0, - - cookiesLost: - 0 + played: 0, + won: 0, + lost: 0, + pushed: 0, + cookiesWon: 0, + cookiesLost: 0 } }, streaks: { - currentWin: - 0, - - bestWin: - 0, - - currentLoss: - 0, - - bestLoss: - 0 + currentWin: 0, + bestWin: 0, + currentLoss: 0, + bestLoss: 0 } }; } - -/* - * ============================================================ - * ENSURE STATS DOCUMENT - * ============================================================ - */ - -async function ensureStatsDocument( - userId, - guildId -) { - const { - stats - } = - getProfileCollections(); - +async function ensureStatsDocument(userId, guildId) { + const { stats } = getProfileCollections(); await stats.updateOne( + { userId, guildId }, { - userId, - guildId + $setOnInsert: getDefaultStats( + userId, + guildId + ) }, - { - $setOnInsert: - getDefaultStats( - userId, - guildId - ) - }, - { - upsert: - true - } + { upsert: true } ); } - -/* - * ============================================================ - * ENSURE ACHIEVEMENT DOCUMENT - * ============================================================ - */ - -async function ensureAchievementDocument( - userId, - guildId -) { - const { - achievements - } = - getProfileCollections(); - +async function ensureAchievementDocument(userId, guildId) { + const { achievements } = getProfileCollections(); await achievements.updateOne( - { - userId, - guildId - }, + { userId, guildId }, { $setOnInsert: { userId, guildId, - unlocked: {} } }, - { - upsert: - true - } + { upsert: true } ); } +async function getProfileStats(userId, guildId) { + const { stats } = getProfileCollections(); -/* - * ============================================================ - * GET PROFILE STATS - * ============================================================ - */ - -async function getProfileStats( - userId, - guildId -) { - const { - stats - } = - getProfileCollections(); - - - const existing = - await stats.findOne({ - userId, - guildId - }); - - - return existing - ?? - getDefaultStats( - userId, - guildId - ); + return await stats.findOne({ + userId, + guildId + }) ?? getDefaultStats( + userId, + guildId + ); } - -/* - * ============================================================ - * TRACK PROFILE EVENT - * ============================================================ - */ - async function trackProfileEvent( interaction, userId, event ) { - const guildId = - interaction.guildId; - + const guildId = interaction.guildId; if ( - !guildId - || - !userId - || + !guildId || + !userId || !event?.type ) { return; } - await ensureStatsDocument( userId, guildId ); + const { stats } = getProfileCollections(); - const { - stats - } = - getProfileCollections(); - - - switch ( - event.type - ) { - /* - * Lifetime gain. - */ - + switch (event.type) { case "cookie_gain": - await stats.updateOne( - { - userId, - guildId - }, + { userId, guildId }, { $inc: { "economy.cookiesObtainedTotal": @@ -480,21 +212,11 @@ async function trackProfileEvent( } } ); - break; - - /* - * Lifetime loss. - */ - case "cookie_loss": - await stats.updateOne( - { - userId, - guildId - }, + { userId, guildId }, { $inc: { "economy.cookiesLostTotal": @@ -502,21 +224,11 @@ async function trackProfileEvent( } } ); - break; - - /* - * Voluntary giving. - */ - case "cookies_given": - await stats.updateOne( - { - userId, - guildId - }, + { userId, guildId }, { $inc: { "economy.cookiesGivenTotal": @@ -524,75 +236,43 @@ async function trackProfileEvent( } } ); - break; - - /* - * Successful player theft. - */ - case "steal_success": - await stats.updateOne( - { - userId, - guildId - }, + { userId, guildId }, { $inc: { "economy.stolenTotal": event.amount }, - $addToSet: { "unique.playersStolenFrom": event.targetUserId } } ); - break; - - /* - * Victim side. - */ - case "stolen_from_me": - await stats.updateOne( - { - userId, - guildId - }, + { userId, guildId }, { $inc: { "economy.stolenFromMeTotal": event.amount }, - $addToSet: { "unique.playersStolenBy": event.thiefUserId } } ); - break; - - /* - * Successful Bot Vault Heist. - */ - case "vault_steal": - await stats.updateOne( - { - userId, - guildId - }, + { userId, guildId }, { $inc: { "economy.vaultStolenTotal": @@ -600,49 +280,32 @@ async function trackProfileEvent( } } ); - break; - - /* - * Game result. - */ - case "game_result": - await trackGameResult( userId, guildId, event ); - break; - default: - return; } - - /* - * Always evaluate from fresh DB state. - */ - const updatedStats = await getProfileStats( userId, guildId ); - await evaluateAchievements( interaction, userId, updatedStats ); - await evaluateBadges( interaction, userId, @@ -650,35 +313,19 @@ async function trackProfileEvent( event ); - return updatedStats; } - -/* - * ============================================================ - * GAME RESULT TRACKING - * ============================================================ - */ - async function trackGameResult( userId, guildId, event ) { - const { - stats - } = - getProfileCollections(); - - - const game = - String( - event.game - ?? - "" - ).toLowerCase(); + const { stats } = getProfileCollections(); + const game = String( + event.game ?? "" + ).toLowerCase(); if ( ![ @@ -686,313 +333,128 @@ async function trackGameResult( "blackjack", "roulette", "heist" - ].includes( - game - ) + ].includes(game) ) { return; } + const result = String( + event.result ?? "" + ).toLowerCase(); - const result = - String( - event.result - ?? - "" - ).toLowerCase(); - - - const profit = - Number( - event.profit - ?? - 0 - ); - + const profit = Number( + event.profit ?? 0 + ); const increment = { - "games.total.played": - 1, - - [`games.${game}.played`]: - 1 + "games.total.played": 1, + [`games.${game}.played`]: 1 }; - const set = {}; - - /* - * ======================================================== - * WIN - * ======================================================== - */ - - if ( - result === - "win" - ) { - increment[ - "games.total.won" - ] = - 1; - - - increment[ - `games.${game}.won` - ] = - 1; - - - increment[ - "streaks.currentWin" - ] = - 1; - - - set[ - "streaks.currentLoss" - ] = - 0; + if (result === "win") { + increment["games.total.won"] = 1; + increment[`games.${game}.won`] = 1; + increment["streaks.currentWin"] = 1; + set["streaks.currentLoss"] = 0; } - - /* - * ======================================================== - * LOSS - * ======================================================== - */ - - else if ( - result === - "loss" - ) { - increment[ - "games.total.lost" - ] = - 1; - - - increment[ - `games.${game}.lost` - ] = - 1; - - - increment[ - "streaks.currentLoss" - ] = - 1; - - - set[ - "streaks.currentWin" - ] = - 0; + else if (result === "loss") { + increment["games.total.lost"] = 1; + increment[`games.${game}.lost`] = 1; + increment["streaks.currentLoss"] = 1; + set["streaks.currentWin"] = 0; } - - /* - * ======================================================== - * PUSH - * ======================================================== - */ - - else if ( - result === - "push" - ) { - increment[ - "games.total.pushed" - ] = - 1; - - - increment[ - `games.${game}.pushed` - ] = - 1; - - - /* - * Push leaves streaks unchanged. - */ + else if (result === "push") { + increment["games.total.pushed"] = 1; + increment[`games.${game}.pushed`] = 1; } - else { return; } - - /* - * ======================================================== - * REALIZED NET COOKIE RESULT - * ======================================================== - * - * Positive: - * - * cookiesWon += profit - * - * Negative: - * - * cookiesLost += abs(profit) - * - * Push: - * - * normally profit = 0 - */ - - if ( - Number.isFinite( - profit - ) - ) { - if ( - profit > - 0 - ) { + if (Number.isFinite(profit)) { + if (profit > 0) { increment[ `games.${game}.cookiesWon` - ] = - profit; + ] = profit; } - - else if ( - profit < - 0 - ) { + else if (profit < 0) { increment[ `games.${game}.cookiesLost` - ] = - Math.abs( - profit - ); + ] = Math.abs(profit); } } - await stats.updateOne( + { userId, guildId }, { - userId, - guildId - }, - { - $inc: - increment, - + $inc: increment, ...(Object.keys(set).length - ? { - $set: - set - } + ? { $set: set } : {}) } ); - - /* - * Update best streaks. - */ - - const current = - await stats.findOne({ - userId, - guildId - }); - + const current = await stats.findOne({ + userId, + guildId + }); await stats.updateOne( - { - userId, - guildId - }, + { userId, guildId }, { $max: { "streaks.bestWin": current?.streaks - ?.currentWin - ?? - 0, + ?.currentWin ?? 0, "streaks.bestLoss": current?.streaks - ?.currentLoss - ?? - 0 + ?.currentLoss ?? 0 } } ); } - -/* - * ============================================================ - * ACHIEVEMENT EVALUATION - * ============================================================ - */ - async function evaluateAchievements( interaction, userId, statsDocument ) { - const guildId = - interaction.guildId; - + const guildId = interaction.guildId; const { achievements, achievementConfig - } = - getProfileCollections(); - + } = getProfileCollections(); await ensureAchievementDocument( userId, guildId ); - - /* - * Only active achievements count toward - * normal evaluation and collector progress. - */ - const definitions = await achievementConfig .find({ active: { - $ne: - false + $ne: false } }) .toArray(); - let unlockedDoc = await achievements.findOne({ userId, guildId }); - let unlocked = - unlockedDoc?.unlocked - ?? - {}; - - - /* - * ======================================================== - * NORMAL ACHIEVEMENTS FIRST - * ======================================================== - * - * Collector achievements are evaluated afterwards, - * because unlocking one normal achievement can itself - * push the user over 50% or 100%. - */ + unlockedDoc?.unlocked ?? {}; const normalDefinitions = definitions.filter( @@ -1001,37 +463,24 @@ async function evaluateAchievements( "achievementPercentage" ); - - for ( - const definition - of normalDefinitions - ) { - if ( - unlocked[ - definition.achievementId - ] - ) { + for (const definition of normalDefinitions) { + if (unlocked[definition.achievementId]) { continue; } - const currentValue = readStatValue( statsDocument, definition ); - if ( currentValue < - Number( - definition.target - ) + Number(definition.target) ) { continue; } - const unlockedNow = await unlockAchievement( interaction, @@ -1040,40 +489,21 @@ async function evaluateAchievements( definition ); - - if ( - unlockedNow - ) { + if (unlockedNow) { unlocked[ definition.achievementId - ] = - getUnixTime(); + ] = getUnixTime(); } } - - /* - * Refresh after normal unlocks. - */ - unlockedDoc = await achievements.findOne({ userId, guildId }); - unlocked = - unlockedDoc?.unlocked - ?? - {}; - - - /* - * ======================================================== - * COLLECTOR ACHIEVEMENTS - * ======================================================== - */ + unlockedDoc?.unlocked ?? {}; const collectorDefinitions = definitions.filter( @@ -1082,15 +512,6 @@ async function evaluateAchievements( "achievementPercentage" ); - - /* - * Collector achievements should NOT count themselves - * toward the denominator. - * - * Otherwise adding "50%" and "100%" achievements changes - * the percentage they are measuring. - */ - const collectibleDefinitions = definitions.filter( definition => @@ -1098,54 +519,37 @@ async function evaluateAchievements( "achievementPercentage" ); - const totalCollectible = collectibleDefinitions.length; - const unlockedCollectible = collectibleDefinitions.filter( definition => Boolean( unlocked[ - definition.achievementId + definition.achievementId ] ) ).length; - const achievementPercentage = - totalCollectible > - 0 + totalCollectible > 0 ? unlockedCollectible / - totalCollectible - + totalCollectible : 0; - - for ( - const definition - of collectorDefinitions - ) { - if ( - unlocked[ - definition.achievementId - ] - ) { + for (const definition of collectorDefinitions) { + if (unlocked[definition.achievementId]) { continue; } - if ( achievementPercentage < - Number( - definition.target - ) + Number(definition.target) ) { continue; } - const unlockedNow = await unlockAchievement( interaction, @@ -1154,50 +558,33 @@ async function evaluateAchievements( definition ); - - if ( - unlockedNow - ) { + if (unlockedNow) { unlocked[ definition.achievementId - ] = - getUnixTime(); + ] = getUnixTime(); } } } - -/* - * ============================================================ - * UNLOCK ACHIEVEMENT - * ============================================================ - */ - async function unlockAchievement( interaction, userId, guildId, definition ) { - const { - achievements - } = + const { achievements } = getProfileCollections(); - const field = `unlocked.${definition.achievementId}`; - const result = await achievements.updateOne( { userId, guildId, - [field]: { - $exists: - false + $exists: false } }, { @@ -1208,82 +595,47 @@ async function unlockAchievement( } ); - - if ( - result.modifiedCount !== - 1 - ) { + if (result.modifiedCount !== 1) { return false; } - await sendAchievementNotification( interaction, definition ); - return true; } - -/* - * ============================================================ - * READ STAT VALUE - * ============================================================ - */ - function readStatValue( document, definition ) { - if ( - !definition.stat - ) { + if (!definition.stat) { return 0; } - const value = definition.stat .split(".") .reduce( - ( - current, - key - ) => + (current, key) => current?.[key], - document ); - if ( definition.measure === "arrayLength" ) { - return Array.isArray( - value - ) + return Array.isArray(value) ? value.length : 0; } - - return Number( - value - ?? - 0 - ); + return Number(value ?? 0); } - -/* - * ============================================================ - * ACHIEVEMENT NOTIFICATION - * ============================================================ - */ - async function sendAchievementNotification( interaction, achievement @@ -1295,7 +647,6 @@ async function sendAchievementNotification( .setTitle( "🏆 Achievement Unlocked!" ) - .setDescription( [ `${achievement.emoji} **${achievement.name}**`, @@ -1304,70 +655,30 @@ async function sendAchievementNotification( ].join("\n") ) ], - flags: MessageFlags.Ephemeral }); - } - - catch { - /* - * Unlock stays persisted even if the Discord - * interaction can no longer accept follow-ups. - */ - } + } catch {} } - -/* - * ============================================================ - * BADGE EVALUATION - * ============================================================ - * - * Existing badges remain supported here. - * - * We can expand this next with: - * - * - first Slots jackpot - * - first natural Blackjack - * - first Roulette Zero - * - first 5 / 10 / 15 unique steals - * - Achievement Hunter - * - Completionist - */ - async function evaluateBadges( interaction, userId, statsDocument, - event = - {} + event = {} ) { - const guildId = - interaction.guildId; - + const guildId = interaction.guildId; if (!guildId) { return; } - - /* - * ======================================================== - * ECONOMY BADGES - * ======================================================== - */ - const balance = await getCookieBalance( userId ); - - if ( - balance >= - 100000 - ) { + if (balance >= 100000) { await tryClaimBadge( interaction, userId, @@ -1375,11 +686,7 @@ async function evaluateBadges( ); } - - if ( - balance >= - 1000000 - ) { + if (balance >= 1000000) { await tryClaimBadge( interaction, userId, @@ -1387,23 +694,12 @@ async function evaluateBadges( ); } - - /* - * ======================================================== - * VAULT BADGE - * ======================================================== - */ - if ( ( statsDocument ?.economy - ?.vaultStolenTotal - ?? - 0 - ) - >= - 100000 + ?.vaultStolenTotal ?? 0 + ) >= 100000 ) { await tryClaimBadge( interaction, @@ -1412,29 +708,15 @@ async function evaluateBadges( ); } - - /* - * ======================================================== - * UNIQUE PLAYER THEFT BADGES - * ======================================================== - */ - const uniquePlayersStolenFrom = statsDocument ?.unique - ?.playersStolenFrom - ?? - []; - + ?.playersStolenFrom ?? []; const uniqueStealCount = uniquePlayersStolenFrom.length; - - if ( - uniqueStealCount >= - 5 - ) { + if (uniqueStealCount >= 5) { await tryClaimBadge( interaction, userId, @@ -1442,11 +724,7 @@ async function evaluateBadges( ); } - - if ( - uniqueStealCount >= - 10 - ) { + if (uniqueStealCount >= 10) { await tryClaimBadge( interaction, userId, @@ -1454,11 +732,7 @@ async function evaluateBadges( ); } - - if ( - uniqueStealCount >= - 15 - ) { + if (uniqueStealCount >= 15) { await tryClaimBadge( interaction, userId, @@ -1466,20 +740,11 @@ async function evaluateBadges( ); } - - /* - * ======================================================== - * SLOT JACKPOT - * ======================================================== - */ - if ( event.type === - "game_result" - && + "game_result" && event.game === - "slots" - && + "slots" && event.jackpot === true ) { @@ -1490,23 +755,13 @@ async function evaluateBadges( ); } - - /* - * ======================================================== - * NATURAL BLACKJACK - * ======================================================== - */ - if ( event.type === - "game_result" - && + "game_result" && event.game === - "blackjack" - && + "blackjack" && event.natural === - true - && + true && event.result === "win" ) { @@ -1517,23 +772,13 @@ async function evaluateBadges( ); } - - /* - * ======================================================== - * BLACKJACK ALL IN - * ======================================================== - */ - if ( event.type === - "game_result" - && + "game_result" && event.game === - "blackjack" - && + "blackjack" && event.allIn === - true - && + true && event.result === "win" ) { @@ -1544,23 +789,13 @@ async function evaluateBadges( ); } - - /* - * ======================================================== - * ROULETTE ZERO - * ======================================================== - */ - if ( event.type === - "game_result" - && + "game_result" && event.game === - "roulette" - && + "roulette" && event.zero === - true - && + true && event.result === "win" ) { @@ -1571,23 +806,13 @@ async function evaluateBadges( ); } - - /* - * ======================================================== - * ROULETTE ALL IN - * ======================================================== - */ - if ( event.type === - "game_result" - && + "game_result" && event.game === - "roulette" - && + "roulette" && event.allIn === - true - && + true && event.result === "win" ) { @@ -1598,13 +823,6 @@ async function evaluateBadges( ); } - - /* - * ======================================================== - * ACHIEVEMENT COLLECTOR BADGES - * ======================================================== - */ - await evaluateCollectorBadges( interaction, userId @@ -1615,30 +833,19 @@ async function evaluateCollectorBadges( interaction, userId ) { - const guildId = - interaction.guildId; - + const guildId = interaction.guildId; const { achievements, achievementConfig - } = - getProfileCollections(); - - - /* - * Don't count Achievement Hunter / Completionist - * themselves toward the percentage. - */ + } = getProfileCollections(); const activeAchievements = await achievementConfig .find({ active: { - $ne: - false + $ne: false }, - measure: { $ne: "achievementPercentage" @@ -1646,7 +853,6 @@ async function evaluateCollectorBadges( }) .toArray(); - if ( activeAchievements.length === 0 @@ -1654,20 +860,14 @@ async function evaluateCollectorBadges( return; } - const achievementDocument = await achievements.findOne({ userId, guildId }); - const unlocked = - achievementDocument - ?.unlocked - ?? - {}; - + achievementDocument?.unlocked ?? {}; const unlockedCount = activeAchievements.filter( @@ -1679,20 +879,11 @@ async function evaluateCollectorBadges( ) ).length; - const percentage = unlockedCount / activeAchievements.length; - - /* - * First player to 50%. - */ - - if ( - percentage >= - 0.5 - ) { + if (percentage >= 0.5) { await tryClaimBadge( interaction, userId, @@ -1700,22 +891,13 @@ async function evaluateCollectorBadges( { unlockedAchievements: unlockedCount, - availableAchievements: activeAchievements.length } ); } - - /* - * First player to 100%. - */ - - if ( - percentage >= - 1 - ) { + if (percentage >= 1) { await tryClaimBadge( interaction, userId, @@ -1723,7 +905,6 @@ async function evaluateCollectorBadges( { unlockedAchievements: unlockedCount, - availableAchievements: activeAchievements.length } @@ -1731,95 +912,43 @@ async function evaluateCollectorBadges( } } - -/* - * ============================================================ - * CLAIM BADGE - * ============================================================ - */ - async function tryClaimBadge( interaction, userId, badgeId, - metadata = - null + metadata = null ) { - const { - badges - } = + const { badges } = getProfileCollections(); + const guildId = interaction.guildId; - const guildId = - interaction.guildId; - - - if ( - !guildId - ) { + if (!guildId) { return false; } - - /* - * Build the fields that should be written - * only when this badge is actually claimed. - */ - const update = { - ownerId: - userId, - - unlockedAt: - getUnixTime() + ownerId: userId, + unlockedAt: getUnixTime() }; - - /* - * Optional historical context. - * - * Example for Completionist: - * - * { - * unlockedAchievements: 64, - * availableAchievements: 64 - * } - */ - if ( - metadata - && + metadata && typeof metadata === "object" ) { - update.metadata = - metadata; + update.metadata = metadata; } - - /* - * Atomic claim. - * - * Only an unclaimed badge can be updated. - * - * If two users qualify at essentially the - * same time, MongoDB lets only one update - * ownerId from null. - */ - const badge = await badges.findOneAndUpdate( { guildId, badgeId, - - ownerId: - null + ownerId: null }, { - $set: - update + $set: update }, { returnDocument: @@ -1827,65 +956,41 @@ async function tryClaimBadge( } ); - - /* - * Badge does not exist, was already claimed, - * or this user did not win the atomic claim. - */ - if ( - !badge - || + !badge || badge.ownerId !== userId ) { return false; } - - /* - * ======================================================== - * PUBLIC ANNOUNCEMENT - * ======================================================== - */ - const description = [ `${badge.emoji ?? "🏅"} **${badge.name ?? badge.badgeId}**`, "", `<@${userId}> claimed a guild-unique badge!`, "", - badge.description - ?? + badge.description ?? "A unique badge has been claimed.", "", "*This badge can no longer be earned by another player.*" ]; - - /* - * Add collector history if applicable. - */ - if ( badge.metadata ?.unlockedAchievements !== - undefined - && + undefined && badge.metadata ?.availableAchievements !== undefined ) { description.splice( - description.length - - 1, + description.length - 1, 0, - `Claimed at **${badge.metadata.unlockedAchievements}/${badge.metadata.availableAchievements} achievements**.`, "" ); } - try { await interaction.channel.send({ embeds: [ @@ -1893,47 +998,26 @@ async function tryClaimBadge( .setTitle( "🏅 Unique Badge Claimed!" ) - .setDescription( - description.join( - "\n" - ) + description.join("\n") ) - .setFooter({ text: `badge:${badge.badgeId}` }) - .setTimestamp() ] }); - } - - catch (error) { - /* - * The badge claim itself is already persisted. - * - * A Discord send failure should never undo it. - */ - + } catch (error) { console.error( `[Badge] Failed to announce ${badgeId} for ${userId}:`, error ); } - return true; } - -/* - * ============================================================ - * PROFILE OVERVIEW - * ============================================================ - */ - async function buildProfileOverview( guildId, user @@ -1944,164 +1028,109 @@ async function buildProfileOverview( guildId ); - const balance = await getCookieBalance( user.id ); - const { achievements, badges, achievementConfig - } = - getProfileCollections(); - + } = getProfileCollections(); const [ achievementDoc, achievementCount, ownedBadges - ] = - await Promise.all([ - achievements.findOne({ - userId: - user.id, + ] = await Promise.all([ + achievements.findOne({ + userId: user.id, + guildId + }), - guildId - }), - - achievementConfig.countDocuments({ - active: { - $ne: - false - } - }), - - badges.find({ - guildId, - - ownerId: - user.id - }).toArray() - ]); + achievementConfig.countDocuments({ + active: { + $ne: false + } + }), + badges.find({ + guildId, + ownerId: user.id + }).toArray() + ]); const unlockedCount = Object.keys( - achievementDoc?.unlocked - ?? - {} + achievementDoc?.unlocked ?? {} ).length; - return new EmbedBuilder() .setTitle( `👤 ${user.globalName ?? user.username}'s Profile` ) - .setThumbnail( user.displayAvatarURL({ - size: - 256 + size: 256 }) ) - .addFields( { - name: - "🍪 Cookies", - + name: "🍪 Cookies", value: balance.toLocaleString(), - - inline: - true + inline: true }, - { - name: - "🏆 Achievements", - + name: "🏆 Achievements", value: `${unlockedCount} / ${achievementCount}`, - - inline: - true + inline: true }, - { - name: - "🏅 Unique Badges", - + name: "🏅 Unique Badges", value: `${ownedBadges.length}`, - - inline: - true + inline: true }, - { - name: - "🎮 Games", - - value: - [ - `Played: **${stats.games.total.played}**`, - `Won: **${stats.games.total.won}**`, - `Lost: **${stats.games.total.lost}**`, - `Pushes: **${stats.games.total.pushed}**` - ].join("\n"), - - inline: - true + name: "🎮 Games", + value: [ + `Played: **${stats.games.total.played}**`, + `Won: **${stats.games.total.won}**`, + `Lost: **${stats.games.total.lost}**`, + `Pushes: **${stats.games.total.pushed}**` + ].join("\n"), + inline: true }, - { - name: - "🔥 Best Streaks", - - value: - [ - `Wins: **${stats.streaks.bestWin}**`, - `Losses: **${stats.streaks.bestLoss}**` - ].join("\n"), - - inline: - true + name: "🔥 Best Streaks", + value: [ + `Wins: **${stats.streaks.bestWin}**`, + `Losses: **${stats.streaks.bestLoss}**` + ].join("\n"), + inline: true } ) - .setFooter({ text: `profile:${user.id}` }); } - -/* - * ============================================================ - * BADGES PAGE - * ============================================================ - */ - async function buildBadgesEmbed( guildId, user ) { - const { - badges - } = + const { badges } = getProfileCollections(); - const documents = await badges.find({ guildId }).toArray(); - const owned = documents.filter( badge => @@ -2109,81 +1138,61 @@ async function buildBadgesEmbed( user.id ); - const claimedByOthers = documents.filter( badge => - badge.ownerId - && + badge.ownerId && badge.ownerId !== - user.id + user.id ); - const unclaimed = documents.filter( badge => !badge.ownerId ); - return new EmbedBuilder() .setTitle( `🏅 ${user.globalName ?? user.username}'s Badges` ) - .setDescription( [ "### Owned", owned.length - ? owned - .map( - badge => - `${badge.emoji} **${badge.name}**\n${badge.description}` - ) - .join("\n\n") + ? owned.map( + badge => + `${badge.emoji} **${badge.name}**\n${badge.description}` + ).join("\n\n") : "*No unique badges yet.*", - "", "### Claimed by Others", claimedByOthers.length - ? claimedByOthers - .map( - badge => - `${badge.emoji} **${badge.name}** — <@${badge.ownerId}>` - ) - .join("\n") + ? claimedByOthers.map( + badge => + `${badge.emoji} **${badge.name}** — <@${badge.ownerId}>` + ).join("\n") : "*None yet.*", - "", "### Still Available", unclaimed.length - ? unclaimed - .map( - badge => - `${badge.emoji} **${badge.name}**\n${badge.description}` - ) - .join("\n\n") + ? unclaimed.map( + badge => + `${badge.emoji} **${badge.name}**\n${badge.description}` + ).join("\n\n") : "*All unique badges have been claimed.*" ].join("\n") ) - .setFooter({ text: `profile:${user.id}` }); } - -/* - * ============================================================ - * ACHIEVEMENTS PAGE - * ============================================================ - */ - async function buildAchievementsEmbed( guildId, - user + user, + page = 0 ) { const { achievements, @@ -2223,18 +1232,32 @@ async function buildAchievementsEmbed( const unlocked = - unlockedDoc?.unlocked - ?? + unlockedDoc?.unlocked ?? {}; + /* + * Keep pages comfortably below Discord's + * 4096-character description limit. + * + * Using a character limit instead of a fixed + * number of achievements also protects us + * against achievements with long names or + * requirements. + */ + + const MAX_PAGE_LENGTH = + 3500; + + const lines = definitions.map( achievement => { const complete = Boolean( unlocked[ - achievement.achievementId + achievement + .achievementId ] ); @@ -2246,7 +1269,8 @@ async function buildAchievementsEmbed( " ", - achievement.emoji, + achievement.emoji ?? + "🏆", " **", @@ -2255,35 +1279,146 @@ async function buildAchievementsEmbed( "** — ", achievement.requirement + ?? + achievement.description + ?? + "Requirement unavailable." ].join(""); } ); + /* + * Build pages dynamically. + */ + + const pages = + []; + + let currentPage = + []; + + + for ( + const line + of lines + ) { + const test = + [ + ...currentPage, + line + ].join( + "\n" + ); + + + if ( + test.length > + MAX_PAGE_LENGTH + && + currentPage.length > + 0 + ) { + pages.push( + currentPage.join( + "\n" + ) + ); + + currentPage = + [ + line + ]; + } + + else { + currentPage.push( + line + ); + } + } + + + if ( + currentPage.length > + 0 + ) { + pages.push( + currentPage.join( + "\n" + ) + ); + } + + + /* + * No achievements configured. + */ + + if ( + pages.length === + 0 + ) { + pages.push( + "No achievements configured." + ); + } + + + /* + * Clamp page so old buttons can't request + * a page that no longer exists after DB + * changes. + */ + + const safePage = + Math.max( + 0, + + Math.min( + Number(page) || 0, + pages.length - 1 + ) + ); + + + const unlockedCount = + definitions.filter( + achievement => + Boolean( + unlocked[ + achievement + .achievementId + ] + ) + ).length; + + return new EmbedBuilder() .setTitle( `🏆 ${user.globalName ?? user.username}'s Achievements` ) .setDescription( - lines.join("\n") - || - "No achievements configured." + pages[ + safePage + ] ) + .addFields({ + name: + "Progress", + + value: + `**${unlockedCount} / ${definitions.length}** achievements unlocked` + }) + .setFooter({ text: - `profile:${user.id}` + `profile:${user.id}:achievements:${safePage}:${pages.length}` }); } - -/* - * ============================================================ - * PROFILE BUTTONS - * ============================================================ - */ - function buildProfileButtons() { return [ new ActionRowBuilder() @@ -2347,13 +1482,6 @@ function buildProfileButtons() { ]; } - -/* - * ============================================================ - * PROFILE TARGET - * ============================================================ - */ - function getProfileTargetId( interaction ) { @@ -2363,44 +1491,23 @@ function getProfileTargetId( ?.footer ?.text; - const [ type, userId - ] = - footer?.split(":") - ?? - []; + ] = footer?.split(":") ?? []; - - if ( - type !== - "profile" - ) { - return null; - } - - - return userId; + return type === "profile" + ? userId + : null; } - -/* - * ============================================================ - * EXPORTS - * ============================================================ - */ - module.exports = { ensureProfileSystem, - trackProfileEvent, getProfileStats, - buildProfileOverview, buildBadgesEmbed, buildAchievementsEmbed, buildProfileButtons, - getProfileTargetId -}; \ No newline at end of file +};