const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } = require("discord.js"); const { mClient } = require("../index"); const { getCookieBalance, getUnixTime } = require("./cookieEconomy"); /* * ============================================================ * CONFIG * ============================================================ */ const DATABASE_NAME = process.env.M_DB; const COLLECTIONS = { stats: "stats_profiles", achievements: "items_achievements", badges: "guild_badges", achievementConfig: "config_achievements" }; const PROFILE_PAGE_CHARACTER_LIMIT = 3500; /* * ============================================================ * COLLECTION ACCESS * ============================================================ */ function getProfileCollections() { if (!DATABASE_NAME) { throw new Error("M_DB is not configured in .env"); } 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) }; } /* * ============================================================ * INDEXES * ============================================================ */ async function ensureProfileSystem() { const { stats, achievements, badges, achievementConfig } = getProfileCollections(); await stats.createIndex( { userId: 1, guildId: 1 }, { unique: true } ); await achievements.createIndex( { userId: 1, guildId: 1 }, { unique: true } ); await badges.createIndex( { guildId: 1, badgeId: 1 }, { unique: true } ); await achievementConfig.createIndex( { achievementId: 1 }, { unique: true } ); } /* * ============================================================ * DEFAULT STATS * ============================================================ */ function getDefaultStats(userId, guildId) { return { userId, guildId, economy: { cookiesObtainedTotal: 0, cookiesLostTotal: 0, cookiesGivenTotal: 0, stolenTotal: 0, stolenFromMeTotal: 0, vaultStolenTotal: 0 }, unique: { playersStolenFrom: [], playersStolenBy: [] }, games: { total: { played: 0, won: 0, lost: 0, pushed: 0 }, slots: { 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 }, roulette: { 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 } }, streaks: { currentWin: 0, bestWin: 0, currentLoss: 0, bestLoss: 0 } }; } /* * ============================================================ * ENSURE DOCUMENTS * ============================================================ */ async function ensureStatsDocument(userId, guildId) { const { stats } = getProfileCollections(); await stats.updateOne( { userId, guildId }, { $setOnInsert: getDefaultStats( userId, guildId ) }, { upsert: true } ); } async function ensureAchievementDocument(userId, guildId) { const { achievements } = getProfileCollections(); await achievements.updateOne( { userId, guildId }, { $setOnInsert: { userId, guildId, unlocked: {} } }, { upsert: true } ); } /* * ============================================================ * GET PROFILE STATS * ============================================================ */ async function getProfileStats(userId, guildId) { const { stats } = getProfileCollections(); const existing = await stats.findOne({ userId, guildId }); return existing ?? getDefaultStats( userId, guildId ); } /* * ============================================================ * PROFILE EVENTS * ============================================================ */ async function trackProfileEvent( interaction, userId, event ) { const guildId = interaction.guildId; if (!guildId || !userId || !event?.type) { return; } await ensureStatsDocument( userId, guildId ); const { stats } = getProfileCollections(); switch (event.type) { case "cookie_gain": await stats.updateOne( { userId, guildId }, { $inc: { "economy.cookiesObtainedTotal": Number(event.amount ?? 0) } } ); break; case "cookie_loss": await stats.updateOne( { userId, guildId }, { $inc: { "economy.cookiesLostTotal": Number(event.amount ?? 0) } } ); break; case "cookies_given": await stats.updateOne( { userId, guildId }, { $inc: { "economy.cookiesGivenTotal": Number(event.amount ?? 0) } } ); break; case "steal_success": await stats.updateOne( { userId, guildId }, { $inc: { "economy.stolenTotal": Number(event.amount ?? 0) }, $addToSet: { "unique.playersStolenFrom": event.targetUserId } } ); break; case "stolen_from_me": await stats.updateOne( { userId, guildId }, { $inc: { "economy.stolenFromMeTotal": Number(event.amount ?? 0) }, $addToSet: { "unique.playersStolenBy": event.thiefUserId } } ); break; case "vault_steal": await stats.updateOne( { userId, guildId }, { $inc: { "economy.vaultStolenTotal": Number(event.amount ?? 0) } } ); break; case "game_result": await trackGameResult( userId, guildId, event ); break; default: return; } const updatedStats = await getProfileStats( userId, guildId ); await evaluateAchievements( interaction, userId, updatedStats ); await evaluateBadges( interaction, userId, updatedStats, event ); return updatedStats; } /* * ============================================================ * CENTRAL GAME RESULT * ============================================================ */ async function recordGameOutcome( interaction, userId, { game, result, profit = 0, ...metadata } ) { const guildId = interaction.guildId; if (!guildId || !userId) { return; } const normalizedProfit = Number( profit ?? 0 ); await ensureStatsDocument( userId, guildId ); const { stats } = getProfileCollections(); /* * One authoritative game event. */ await trackGameResult( userId, guildId, { game, result, profit: normalizedProfit, ...metadata } ); /* * Global economy totals. * * Games using recordGameOutcome() should NOT also send * cookie_gain / cookie_loss events for the same result. */ if (normalizedProfit > 0) { await stats.updateOne( { userId, guildId }, { $inc: { "economy.cookiesObtainedTotal": normalizedProfit } } ); } else if (normalizedProfit < 0) { await stats.updateOne( { userId, guildId }, { $inc: { "economy.cookiesLostTotal": Math.abs(normalizedProfit) } } ); } const updatedStats = await getProfileStats( userId, guildId ); const event = { type: "game_result", game, result, profit: normalizedProfit, ...metadata }; await evaluateAchievements( interaction, userId, updatedStats ); await evaluateBadges( interaction, userId, updatedStats, event ); return updatedStats; } /* * ============================================================ * GAME STATS * ============================================================ */ async function trackGameResult( userId, guildId, event ) { const { stats } = getProfileCollections(); const game = String( event.game ?? "" ).toLowerCase(); if ( ![ "slots", "blackjack", "roulette", "heist" ].includes(game) ) { return; } const result = String( event.result ?? "" ).toLowerCase(); const profit = Number( event.profit ?? 0 ); const increment = { "games.total.played": 1, [`games.${game}.played`]: 1 }; const set = {}; if (result === "win") { increment["games.total.won"] = 1; increment[`games.${game}.won`] = 1; increment["streaks.currentWin"] = 1; set["streaks.currentLoss"] = 0; } 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 === "push") { increment["games.total.pushed"] = 1; increment[`games.${game}.pushed`] = 1; } else { return; } if (Number.isFinite(profit)) { if (profit > 0) { increment[ `games.${game}.cookiesWon` ] = profit; } else if (profit < 0) { increment[ `games.${game}.cookiesLost` ] = Math.abs(profit); } } await stats.updateOne( { userId, guildId }, { $inc: increment, ...(Object.keys(set).length ? { $set: set } : {}) } ); const current = await stats.findOne({ userId, guildId }); await stats.updateOne( { userId, guildId }, { $max: { "streaks.bestWin": current?.streaks?.currentWin ?? 0, "streaks.bestLoss": current?.streaks?.currentLoss ?? 0 } } ); } /* * ============================================================ * ACHIEVEMENT EVALUATION * ============================================================ */ async function evaluateAchievements( interaction, userId, statsDocument ) { const guildId = interaction.guildId; if (!guildId) { return; } const { achievements, achievementConfig } = getProfileCollections(); await ensureAchievementDocument( userId, guildId ); const definitions = await achievementConfig .find({ active: { $ne: false } }) .toArray(); let unlockedDocument = await achievements.findOne({ userId, guildId }); let unlocked = unlockedDocument?.unlocked ?? {}; /* * Normal DB-backed achievements first. */ const normalDefinitions = definitions.filter( definition => definition.measure !== "achievementPercentage" ); for (const definition of normalDefinitions) { if ( unlocked[ definition.achievementId ] ) { continue; } /* * Event-only achievements do not have a persisted * stat path and are intentionally skipped here. */ if (!definition.stat) { continue; } const currentValue = readStatValue( statsDocument, definition ); if ( currentValue < Number(definition.target) ) { continue; } const unlockedNow = await unlockAchievement( interaction, userId, guildId, definition ); if (unlockedNow) { unlocked[ definition.achievementId ] = getUnixTime(); } } /* * Refresh after normal unlocks. */ unlockedDocument = await achievements.findOne({ userId, guildId }); unlocked = unlockedDocument?.unlocked ?? {}; /* * Collector achievements. */ const collectorDefinitions = definitions.filter( definition => definition.measure === "achievementPercentage" ); const collectibleDefinitions = definitions.filter( definition => definition.measure !== "achievementPercentage" ); const totalCollectible = collectibleDefinitions.length; const unlockedCollectible = collectibleDefinitions.filter( definition => Boolean( unlocked[ definition.achievementId ] ) ).length; const percentage = totalCollectible > 0 ? unlockedCollectible / totalCollectible : 0; for (const definition of collectorDefinitions) { if ( unlocked[ definition.achievementId ] ) { continue; } if ( percentage < Number(definition.target) ) { continue; } const unlockedNow = await unlockAchievement( interaction, userId, guildId, definition ); if (unlockedNow) { unlocked[ definition.achievementId ] = getUnixTime(); } } } /* * ============================================================ * UNLOCK ACHIEVEMENT * ============================================================ */ async function unlockAchievement( interaction, userId, guildId, definition ) { const { achievements } = getProfileCollections(); const field = `unlocked.${definition.achievementId}`; const result = await achievements.updateOne( { userId, guildId, [field]: { $exists: false } }, { $set: { [field]: getUnixTime() } } ); if (result.modifiedCount !== 1) { return false; } await sendAchievementNotification( interaction, definition ); return true; } /* * ============================================================ * STAT READER * ============================================================ */ function readStatValue( document, definition ) { if (!definition.stat) { return 0; } const value = definition.stat .split(".") .reduce( (current, key) => current?.[key], document ); if ( definition.measure === "arrayLength" ) { return Array.isArray(value) ? value.length : 0; } return Number( value ?? 0 ); } /* * ============================================================ * ACHIEVEMENT NOTIFICATION * ============================================================ * * Public by default. */ async function sendAchievementNotification( interaction, achievement ) { const embed = new EmbedBuilder() .setTitle( "🏆 Achievement Unlocked!" ) .setDescription( [ `${achievement.emoji ?? "🏆"} **${achievement.name}**`, "", achievement.description ?? achievement.requirement ?? "Achievement unlocked." ].join("\n") ) .setTimestamp(); try { if ( interaction.replied || interaction.deferred ) { await interaction.followUp({ embeds: [embed] }); return; } /* * Do not consume the primary interaction reply. */ if (interaction.channel) { await interaction.channel.send({ embeds: [embed] }); } } catch (error) { console.error( `[Achievement] Failed to announce ${achievement.achievementId}:`, error ); } } /* * ============================================================ * BADGE EVALUATION * ============================================================ */ async function evaluateBadges( interaction, userId, statsDocument, event = {} ) { const guildId = interaction.guildId; if (!guildId) { return; } const balance = await getCookieBalance( userId ); /* * Economy badges. */ if (balance >= 100000) { await tryClaimBadge( interaction, userId, "first_100k_cookies" ); } if (balance >= 1000000) { await tryClaimBadge( interaction, userId, "first_1m_cookies" ); } /* * Vault milestone. */ if ( ( statsDocument ?.economy ?.vaultStolenTotal ?? 0 ) >= 100000 ) { await tryClaimBadge( interaction, userId, "first_100k_vault_stolen" ); } /* * Unique player theft milestones. */ const uniqueStealCount = ( statsDocument ?.unique ?.playersStolenFrom ?? [] ).length; if (uniqueStealCount >= 5) { await tryClaimBadge( interaction, userId, "first_unique_steals_5" ); } if (uniqueStealCount >= 10) { await tryClaimBadge( interaction, userId, "first_unique_steals_10" ); } if (uniqueStealCount >= 15) { await tryClaimBadge( interaction, userId, "first_unique_steals_15" ); } /* * Slots jackpot. */ if ( event.type === "game_result" && event.game === "slots" && event.jackpot === true ) { await tryClaimBadge( interaction, userId, "first_slots_jackpot" ); } /* * Natural Blackjack. */ if ( event.type === "game_result" && event.game === "blackjack" && event.natural === true && event.result === "win" ) { await tryClaimBadge( interaction, userId, "first_blackjack_natural" ); } /* * Blackjack All-In win. */ if ( event.type === "game_result" && event.game === "blackjack" && event.allIn === true && event.result === "win" ) { await tryClaimBadge( interaction, userId, "first_blackjack_allin_win" ); } /* * Roulette Zero. */ if ( event.type === "game_result" && event.game === "roulette" && event.zero === true && event.result === "win" ) { await tryClaimBadge( interaction, userId, "first_roulette_zero" ); } /* * Roulette All-In. */ if ( event.type === "game_result" && event.game === "roulette" && event.allIn === true && event.result === "win" ) { await tryClaimBadge( interaction, userId, "first_roulette_allin_win" ); } await evaluateCollectorBadges( interaction, userId ); } /* * ============================================================ * COLLECTOR BADGES * ============================================================ */ async function evaluateCollectorBadges( interaction, userId ) { const guildId = interaction.guildId; const { achievements, achievementConfig } = getProfileCollections(); const activeAchievements = await achievementConfig .find({ active: { $ne: false }, measure: { $ne: "achievementPercentage" } }) .toArray(); if ( activeAchievements.length === 0 ) { return; } const achievementDocument = await achievements.findOne({ userId, guildId }); const unlocked = achievementDocument?.unlocked ?? {}; const unlockedCount = activeAchievements.filter( achievement => Boolean( unlocked[ achievement.achievementId ] ) ).length; const percentage = unlockedCount / activeAchievements.length; if (percentage >= 0.5) { await tryClaimBadge( interaction, userId, "first_achievement_half", { unlockedAchievements: unlockedCount, availableAchievements: activeAchievements.length } ); } if (percentage >= 1) { await tryClaimBadge( interaction, userId, "first_achievement_all", { unlockedAchievements: unlockedCount, availableAchievements: activeAchievements.length } ); } } /* * ============================================================ * CLAIM GLOBAL BADGE * ============================================================ */ async function tryClaimBadge( interaction, userId, badgeId, metadata = null ) { const { badges } = getProfileCollections(); const guildId = interaction.guildId; if (!guildId) { return false; } const update = { ownerId: userId, unlockedAt: getUnixTime() }; if ( metadata && typeof metadata === "object" ) { update.metadata = metadata; } const badge = await badges.findOneAndUpdate( { guildId, badgeId, ownerId: null }, { $set: update }, { returnDocument: "after" } ); if ( !badge || badge.ownerId !== userId ) { return false; } const description = [ `${badge.emoji ?? "🏅"} **${badge.name ?? badge.badgeId}**`, "", `<@${userId}> claimed a guild-unique badge!`, "", badge.description ?? "A unique badge has been claimed.", "" ]; if ( badge.metadata ?.unlockedAchievements !== undefined && badge.metadata ?.availableAchievements !== undefined ) { description.push( `Claimed at **${badge.metadata.unlockedAchievements}/${badge.metadata.availableAchievements} achievements**.`, "" ); } description.push( "*This badge can no longer be earned by another player.*" ); try { await interaction.channel.send({ embeds: [ new EmbedBuilder() .setTitle( "🏅 Unique Badge Claimed!" ) .setDescription( description.join("\n") ) .setFooter({ text: `badge:${badge.badgeId}` }) .setTimestamp() ] }); } catch (error) { console.error( `[Badge] Failed to announce ${badgeId} for ${userId}:`, error ); } return true; } /* * ============================================================ * PROFILE FOOTER / SESSION STATE * ============================================================ * * profile:::
:: */ function buildProfileFooter( sessionOwnerId, profileUserId, section = "overview", page = 0, pageCount = 1 ) { return [ "profile", sessionOwnerId, profileUserId, section, page, pageCount ].join(":"); } function parseProfileFooter( interaction ) { const footer = interaction.message ?.embeds?.[0] ?.footer ?.text; const parts = footer?.split(":") ?? []; if ( parts[0] !== "profile" ) { return null; } /* * Current footer. */ if ( parts.length >= 6 ) { return { sessionOwnerId: parts[1], profileUserId: parts[2], section: parts[3], page: Number(parts[4]) || 0, pageCount: Number(parts[5]) || 1 }; } /* * Legacy footer: * * profile: */ if ( parts.length >= 2 ) { return { sessionOwnerId: parts[1], profileUserId: parts[1], section: "overview", page: 0, pageCount: 1 }; } return null; } function getProfileTargetId( interaction ) { return ( parseProfileFooter( interaction )?.profileUserId ?? null ); } function getProfileSessionOwnerId( interaction ) { return ( parseProfileFooter( interaction )?.sessionOwnerId ?? null ); } /* * ============================================================ * PROFILE OWNERSHIP * ============================================================ */ async function requireProfileOwner( interaction ) { const session = parseProfileFooter( interaction ); if (!session) { await temporaryProfileError( interaction, "This profile session is no longer valid." ); return null; } if ( session.sessionOwnerId !== interaction.user.id ) { await temporaryProfileError( interaction, "This isn't your profile session." ); return null; } return session; } async function temporaryProfileError( interaction, content ) { const payload = { content, flags: MessageFlags.Ephemeral }; if ( interaction.replied || interaction.deferred ) { return interaction.followUp( payload ); } return interaction.reply( payload ); } /* * ============================================================ * PROFILE BUTTONS * ============================================================ */ function buildProfileButtons() { return [ new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId( "profileOverview" ) .setLabel( "Overview" ) .setEmoji( "👤" ) .setStyle( ButtonStyle.Primary ), new ButtonBuilder() .setCustomId( "profileStats" ) .setLabel( "Stats" ) .setEmoji( "📊" ) .setStyle( ButtonStyle.Secondary ), new ButtonBuilder() .setCustomId( "profileAchievements" ) .setLabel( "Achievements" ) .setEmoji( "🏆" ) .setStyle( ButtonStyle.Secondary ), new ButtonBuilder() .setCustomId( "profileBadges" ) .setLabel( "Badges" ) .setEmoji( "🏅" ) .setStyle( ButtonStyle.Secondary ), new ButtonBuilder() .setCustomId( "profileClose" ) .setLabel( "Close" ) .setEmoji( "✖️" ) .setStyle( ButtonStyle.Secondary ) ) ]; } function buildProfilePaginationButtons( leftId, rightId, page, pageCount ) { return new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId( leftId ) .setLabel( "◄" ) .setStyle( ButtonStyle.Secondary ) .setDisabled( page <= 0 ), new ButtonBuilder() .setCustomId( rightId ) .setLabel( "►" ) .setStyle( ButtonStyle.Secondary ) .setDisabled( page >= pageCount - 1 ) ); } /* * ============================================================ * PROFILE OVERVIEW * ============================================================ */ async function buildProfileOverview( guildId, user, sessionOwnerId = user.id ) { const stats = await getProfileStats( user.id, guildId ); const balance = await getCookieBalance( user.id ); const { achievements, badges, achievementConfig } = getProfileCollections(); const [ achievementDocument, activeDefinitions, ownedBadges ] = await Promise.all([ achievements.findOne({ userId: user.id, guildId }), achievementConfig .find({ active: { $ne: false } }) .toArray(), badges.find({ guildId, ownerId: user.id }).toArray() ]); const unlocked = achievementDocument?.unlocked ?? {}; const unlockedCount = activeDefinitions.filter( achievement => Boolean( unlocked[ achievement.achievementId ] ) ).length; const achievementCount = activeDefinitions.length; const completion = achievementCount > 0 ? ( unlockedCount / achievementCount ) * 100 : 0; return new EmbedBuilder() .setTitle( `👤 ${user.globalName ?? user.username}'s Profile` ) .setThumbnail( user.displayAvatarURL({ size: 256 }) ) .addFields( { name: "🍪 Cookies", value: balance.toLocaleString(), inline: true }, { name: "🏆 Achievements", value: `${unlockedCount} / ${achievementCount}\n**${completion.toFixed(1)}%**`, inline: true }, { name: "🏅 Unique Badges", value: `${ownedBadges.length}`, inline: true }, { name: "🎮 Games", value: [ `Played: **${stats.games?.total?.played ?? 0}**`, `Won: **${stats.games?.total?.won ?? 0}**`, `Lost: **${stats.games?.total?.lost ?? 0}**`, `Pushes: **${stats.games?.total?.pushed ?? 0}**` ].join("\n"), inline: true }, { name: "🔥 Best Streaks", value: [ `Wins: **${stats.streaks?.bestWin ?? 0}**`, `Losses: **${stats.streaks?.bestLoss ?? 0}**` ].join("\n"), inline: true } ) .setFooter({ text: buildProfileFooter( sessionOwnerId, user.id, "overview", 0, 1 ) }); } /* * ============================================================ * STATS PAGE * ============================================================ */ async function buildProfileStatsEmbed( guildId, user, sessionOwnerId = user.id ) { const stats = await getProfileStats( user.id, guildId ); const embed = new EmbedBuilder() .setTitle( `📊 ${user.globalName ?? user.username}'s Stats` ) .setThumbnail( user.displayAvatarURL({ size: 256 }) ); const games = [ [ "🎰", "Slots", stats.games?.slots ], [ "🃏", "Blackjack", stats.games?.blackjack ], [ "🎡", "Roulette", stats.games?.roulette ], [ "🏦", "Heist", stats.games?.heist ] ]; for ( const [ emoji, name, game ] of games ) { const played = game?.played ?? 0; const won = game?.won ?? 0; const lost = game?.lost ?? 0; const pushed = game?.pushed ?? 0; const cookiesWon = game?.cookiesWon ?? 0; const cookiesLost = game?.cookiesLost ?? 0; const net = cookiesWon - cookiesLost; const decidedGames = won + lost; const winRate = decidedGames > 0 ? ( won / decidedGames ) * 100 : 0; embed.addFields({ name: `${emoji} ${name}`, value: [ `Played: **${played.toLocaleString()}**`, `W / L / P: **${won} / ${lost} / ${pushed}**`, `Win Rate: **${winRate.toFixed(1)}%**`, `Won: **${cookiesWon.toLocaleString()} 🍪**`, `Lost: **${cookiesLost.toLocaleString()} 🍪**`, `Net: **${net >= 0 ? "+" : ""}${net.toLocaleString()} 🍪**` ].join("\n"), inline: true }); } embed.addFields({ name: "🍪 Economy", value: [ `Obtained: **${(stats.economy?.cookiesObtainedTotal ?? 0).toLocaleString()} 🍪**`, `Lost: **${(stats.economy?.cookiesLostTotal ?? 0).toLocaleString()} 🍪**`, `Given: **${(stats.economy?.cookiesGivenTotal ?? 0).toLocaleString()} 🍪**`, `Stolen: **${(stats.economy?.stolenTotal ?? 0).toLocaleString()} 🍪**`, `Stolen from you: **${(stats.economy?.stolenFromMeTotal ?? 0).toLocaleString()} 🍪**`, `Vault stolen: **${(stats.economy?.vaultStolenTotal ?? 0).toLocaleString()} 🍪**` ].join("\n") }); embed.setFooter({ text: buildProfileFooter( sessionOwnerId, user.id, "stats", 0, 1 ) }); return embed; } /* * ============================================================ * ACHIEVEMENTS PAGE * ============================================================ */ async function buildAchievementsEmbed( guildId, user, page = 0, sessionOwnerId = user.id ) { const { achievements, achievementConfig } = getProfileCollections(); const [ unlockedDocument, definitions ] = await Promise.all([ achievements.findOne({ userId: user.id, guildId }), achievementConfig .find({ active: { $ne: false } }) .sort({ category: 1, target: 1, name: 1 }) .toArray() ]); const unlocked = unlockedDocument?.unlocked ?? {}; const lines = []; let lastCategory = null; for (const achievement of definitions) { const complete = Boolean( unlocked[ achievement.achievementId ] ); const category = achievement.category ?? "other"; if ( category !== lastCategory ) { lines.push( `### ${formatCategoryName(category)}` ); lastCategory = category; } const hidden = achievement.secret === true && !complete; if (hidden) { lines.push( "⬜ ❓ **???** — Hidden achievement." ); continue; } lines.push( [ complete ? "✅" : "⬜", " ", achievement.emoji ?? "🏆", " **", achievement.name, "** — ", achievement.requirement ?? achievement.description ?? "Requirement unavailable." ].join("") ); } const pages = paginateLines( lines, PROFILE_PAGE_CHARACTER_LIMIT ); const safePage = clampPage( page, pages.length ); const unlockedCount = definitions.filter( achievement => Boolean( unlocked[ achievement.achievementId ] ) ).length; const embed = new EmbedBuilder() .setTitle( `🏆 ${user.globalName ?? user.username}'s Achievements` ) .setDescription( pages[safePage] ?? "No achievements configured." ) .addFields({ name: "Progress", value: `**${unlockedCount} / ${definitions.length}** achievements unlocked` }) .setFooter({ text: buildProfileFooter( sessionOwnerId, user.id, "achievements", safePage, pages.length ) }); return { embed, page: safePage, pageCount: pages.length }; } /* * ============================================================ * BADGES PAGE * ============================================================ */ async function buildBadgesEmbed( guildId, user, page = 0, sessionOwnerId = user.id ) { const { badges } = getProfileCollections(); const documents = await badges .find({ guildId }) .sort({ category: 1, name: 1 }) .toArray(); const categories = groupBadgesByCategory( documents ); const pages = []; /* * Page 1: * badges owned by viewed user. */ const owned = documents.filter( badge => badge.ownerId === user.id ); pages.push({ title: "Owned Badges", description: owned.length ? owned .map( badge => formatBadgeLine( badge, user.id ) ) .join("\n\n") : "*No unique badges owned yet.*" }); /* * Remaining pages: * derived from MongoDB categories. */ for ( const [ category, categoryBadges ] of categories ) { pages.push({ title: formatCategoryName( category ), description: categoryBadges .map( badge => formatBadgeLine( badge, user.id ) ) .join("\n\n") }); } const safePage = clampPage( page, pages.length ); const current = pages[safePage] ?? { title: "Badges", description: "No badges configured." }; const embed = new EmbedBuilder() .setTitle( `🏅 ${user.globalName ?? user.username}'s Badges` ) .setDescription( [ `### ${current.title}`, "", current.description ].join("\n") ) .setFooter({ text: buildProfileFooter( sessionOwnerId, user.id, "badges", safePage, pages.length ) }); return { embed, page: safePage, pageCount: pages.length }; } /* * ============================================================ * BADGE DISPLAY HELPERS * ============================================================ */ function groupBadgesByCategory( badges ) { const categories = new Map(); for (const badge of badges) { const category = badge.category ?? inferBadgeCategory( badge.badgeId ); if ( !categories.has( category ) ) { categories.set( category, [] ); } categories .get(category) .push(badge); } return categories; } function inferBadgeCategory( badgeId = "" ) { if ( badgeId.includes( "slots" ) ) { return "slots"; } if ( badgeId.includes( "blackjack" ) ) { return "blackjack"; } if ( badgeId.includes( "roulette" ) ) { return "roulette"; } if ( badgeId.includes( "achievement" ) ) { return "collector"; } if ( badgeId.includes( "steal" ) ) { return "stealing"; } if ( badgeId.includes( "vault" ) ) { return "heist"; } return "economy"; } function formatBadgeLine( badge, profileUserId ) { const hidden = badge.secret === true && !badge.ownerId; if (hidden) { return [ "❓ **???**", "*Hidden global badge.*" ].join("\n"); } let status; if ( badge.ownerId === profileUserId ) { status = "✅ **Owned by this player**"; } else if ( badge.ownerId ) { status = `🔒 Claimed by <@${badge.ownerId}>`; } else { status = "✨ **Still available**"; } const lines = [ `${badge.emoji ?? "🏅"} **${badge.name ?? badge.badgeId}**`, badge.description ?? "No description.", status ]; if (badge.unlockedAt) { lines.push( `Claimed ` ); } if ( badge.metadata ?.unlockedAchievements !== undefined && badge.metadata ?.availableAchievements !== undefined ) { lines.push( `Claimed at **${badge.metadata.unlockedAchievements}/${badge.metadata.availableAchievements} achievements**` ); } return lines.join("\n"); } /* * ============================================================ * RECONCILE ACHIEVEMENTS * ============================================================ */ async function reconcileProfileAchievements( guildId, { userId = null, dryRun = false } = {} ) { const { stats, achievements, achievementConfig } = getProfileCollections(); const profileFilter = { guildId }; if (userId) { profileFilter.userId = userId; } const [ profileDocuments, definitions ] = await Promise.all([ stats.find( profileFilter ).toArray(), achievementConfig .find({ active: { $ne: false } }) .toArray() ]); const normalDefinitions = definitions.filter( definition => definition.measure !== "achievementPercentage" && Boolean( definition.stat ) ); const eventOnlyDefinitions = definitions.filter( definition => definition.measure !== "achievementPercentage" && !definition.stat ); let unlocksFound = 0; let unlocksApplied = 0; const details = []; for (const profile of profileDocuments) { const profileUserId = profile.userId; if (!profileUserId) { continue; } const achievementDocument = await achievements.findOne({ userId: profileUserId, guildId }); const unlocked = achievementDocument?.unlocked ?? {}; for (const definition of normalDefinitions) { if ( unlocked[ definition.achievementId ] ) { continue; } const currentValue = readStatValue( profile, definition ); if ( currentValue < Number(definition.target) ) { continue; } unlocksFound++; details.push({ userId: profileUserId, achievementId: definition.achievementId, achievementName: definition.name }); if (dryRun) { continue; } await ensureAchievementDocument( profileUserId, guildId ); const field = `unlocked.${definition.achievementId}`; const result = await achievements.updateOne( { userId: profileUserId, guildId, [field]: { $exists: false } }, { $set: { [field]: getUnixTime() } } ); if ( result.modifiedCount === 1 ) { unlocksApplied++; } } /* * Collector achievements. */ const collectorDefinitions = definitions.filter( definition => definition.measure === "achievementPercentage" ); if ( collectorDefinitions.length === 0 ) { continue; } const refreshed = dryRun ? unlocked : ( await achievements.findOne({ userId: profileUserId, guildId }) )?.unlocked ?? {}; const collectible = definitions.filter( definition => definition.measure !== "achievementPercentage" ); const achieved = collectible.filter( definition => { if ( refreshed[ definition.achievementId ] ) { return true; } if ( dryRun && definition.stat ) { return ( readStatValue( profile, definition ) >= Number( definition.target ) ); } return false; } ).length; const percentage = collectible.length > 0 ? achieved / collectible.length : 0; for ( const definition of collectorDefinitions ) { if ( refreshed[ definition.achievementId ] ) { continue; } if ( percentage < Number(definition.target) ) { continue; } unlocksFound++; details.push({ userId: profileUserId, achievementId: definition.achievementId, achievementName: definition.name }); if (dryRun) { continue; } const field = `unlocked.${definition.achievementId}`; const result = await achievements.updateOne( { userId: profileUserId, guildId, [field]: { $exists: false } }, { $set: { [field]: getUnixTime() } } ); if ( result.modifiedCount === 1 ) { unlocksApplied++; } } } return { profilesChecked: profileDocuments.length, unlocksFound, unlocksApplied, skippedEventOnly: eventOnlyDefinitions.length, details }; } /* * ============================================================ * PROFILE SYSTEM AUDIT * ============================================================ */ async function auditProfileSystem( guildId ) { const { stats, achievements, badges, achievementConfig } = getProfileCollections(); const [ profileDocuments, achievementDocuments, activeAchievements, legacyAchievements, badgeDocuments, definitions ] = await Promise.all([ stats.countDocuments({ guildId }), achievements.countDocuments({ guildId }), achievementConfig.countDocuments({ active: { $ne: false } }), achievementConfig.countDocuments({ legacy: true }), badges.find({ guildId }).toArray(), achievementConfig .find({}) .toArray() ]); const claimedBadges = badgeDocuments.filter( badge => Boolean( badge.ownerId ) ).length; const availableBadges = badgeDocuments.length - claimedBadges; const warnings = []; /* * ============================================================ * BADGE VALIDATION * ============================================================ */ const badgeIds = new Set(); for (const badge of badgeDocuments) { if (!badge.badgeId) { warnings.push( `Badge document ${badge._id} has no badgeId.` ); continue; } if ( badgeIds.has( badge.badgeId ) ) { warnings.push( `Duplicate badgeId detected: ${badge.badgeId}` ); } badgeIds.add( badge.badgeId ); } const expectedBadgeIds = [ "first_100k_cookies", "first_1m_cookies", "first_100k_vault_stolen", "first_unique_steals_5", "first_unique_steals_10", "first_unique_steals_15", "first_slots_jackpot", "first_blackjack_natural", "first_blackjack_allin_win", "first_roulette_zero", "first_roulette_allin_win", "first_achievement_half", "first_achievement_all" ]; for (const badgeId of expectedBadgeIds) { if ( !badgeIds.has( badgeId ) ) { warnings.push( `Expected guild badge is missing: ${badgeId}` ); } } /* * ============================================================ * ACHIEVEMENT CONFIG VALIDATION * ============================================================ */ const achievementIds = new Set(); for (const achievement of definitions) { if (!achievement.achievementId) { warnings.push( `Achievement config ${achievement._id} has no achievementId.` ); continue; } if ( achievementIds.has( achievement.achievementId ) ) { warnings.push( `Duplicate achievementId detected: ${achievement.achievementId}` ); } achievementIds.add( achievement.achievementId ); if ( achievement.active !== false && achievement.measure !== "achievementPercentage" && !achievement.stat ) { warnings.push( `Active achievement ${achievement.achievementId} has no stat path.` ); } if ( achievement.target === undefined ) { warnings.push( `Achievement ${achievement.achievementId} has no target.` ); } } /* * ============================================================ * ORPHANED UNLOCKS * ============================================================ */ const unlockedDocuments = await achievements .find({ guildId }) .toArray(); const orphanedUnlocks = new Set(); for (const document of unlockedDocuments) { const unlocked = document.unlocked ?? {}; for ( const achievementId of Object.keys(unlocked) ) { if ( !achievementIds.has( achievementId ) ) { orphanedUnlocks.add( achievementId ); } } } for ( const achievementId of orphanedUnlocks ) { warnings.push( `Unlocked achievement has no config definition: ${achievementId}` ); } /* * ============================================================ * PROFILE SANITY * ============================================================ */ const profileDocs = await stats .find({ guildId }) .toArray(); for (const profile of profileDocs) { if (!profile.userId) { warnings.push( `Profile document ${profile._id} has no userId.` ); } if (!profile.games) { warnings.push( `Profile for ${profile.userId ?? profile._id} has no games object.` ); } if (!profile.economy) { warnings.push( `Profile for ${profile.userId ?? profile._id} has no economy object.` ); } } return { profileDocuments, achievementDocuments, activeAchievements, legacyAchievements, badges: badgeDocuments.length, claimedBadges, availableBadges, warnings }; } /* * ============================================================ * COMPATIBLITY HELPERS * ============================================================ */ const buildStatsEmbed = buildProfileStatsEmbed; /* * ============================================================ * PAGINATION HELPERS * ============================================================ */ function paginateLines( lines, maxLength = PROFILE_PAGE_CHARACTER_LIMIT ) { const pages = []; let current = []; for (const line of lines) { const candidate = [ ...current, line ].join("\n"); if ( candidate.length > maxLength && current.length > 0 ) { pages.push( current.join("\n") ); current = [ line ]; } else { current.push( line ); } } if ( current.length > 0 ) { pages.push( current.join("\n") ); } if ( pages.length === 0 ) { pages.push( "Nothing to display." ); } return pages; } function clampPage( page, pageCount ) { const numeric = Number(page) || 0; return Math.max( 0, Math.min( numeric, Math.max( 0, pageCount - 1 ) ) ); } function formatCategoryName( category ) { return String( category ?? "other" ) .replace( /[_-]+/g, " " ) .replace( /\b\w/g, character => character.toUpperCase() ); } /* * ============================================================ * EXPORTS * ============================================================ */ module.exports = { ensureProfileSystem, getProfileCollections, getProfileStats, getDefaultStats, trackProfileEvent, recordGameOutcome, evaluateAchievements, evaluateBadges, tryClaimBadge, buildProfileOverview, buildProfileStatsEmbed, buildStatsEmbed, buildAchievementsEmbed, buildBadgesEmbed, buildProfileButtons, buildProfilePaginationButtons, buildProfileFooter, parseProfileFooter, getProfileTargetId, getProfileSessionOwnerId, requireProfileOwner, reconcileProfileAchievements, auditProfileSystem };