diff --git a/buttons/profile/achievements.js b/buttons/profile/achievements.js index d9f5038..e4ce1b5 100644 --- a/buttons/profile/achievements.js +++ b/buttons/profile/achievements.js @@ -1,22 +1,54 @@ const { buildAchievementsEmbed, buildProfileButtons, - getProfileTargetId -} = require("../../utils/profileSystem"); + getProfileContextFromFooter, + requireProfileOwner +} = require( + "../../utils/profileSystem" +); module.exports = { - name: "profileAchievements", + name: + "profileAchievements", - async execute(interaction) { - const userId = getProfileTargetId(interaction); - if (!userId) return; + async execute( + interaction + ) { + const context = + await requireProfileOwner( + interaction + ); - const user = await interaction.client.users.fetch(userId); - const embed = await buildAchievementsEmbed(interaction.guildId, user); + if (!context) { + return; + } + + const user = + await interaction.client.users.fetch( + context.profileUserId + ); + + const embed = + await buildAchievementsEmbed( + interaction.guildId, + user, + context.sessionOwnerId, + 0 + ); + + const pageContext = + getProfileContextFromFooter( + embed.data.footer?.text + ); return interaction.update({ embeds: [embed], - components: buildProfileButtons() + components: + buildProfileButtons( + "achievements", + pageContext?.page ?? 0, + pageContext?.pageCount ?? 1 + ) }); } }; diff --git a/buttons/profile/achievementsLeft.js b/buttons/profile/achievementsLeft.js new file mode 100644 index 0000000..7e72918 --- /dev/null +++ b/buttons/profile/achievementsLeft.js @@ -0,0 +1,54 @@ +const { + buildAchievementsEmbed, + buildProfileButtons, + getProfileContextFromFooter, + requireProfileOwner +} = require( + "../../utils/profileSystem" +); + +module.exports = { + name: + "profileAchievementsLeft", + + async execute( + interaction + ) { + const context = + await requireProfileOwner( + interaction + ); + + if (!context) { + return; + } + + const user = + await interaction.client.users.fetch( + context.profileUserId + ); + + const embed = + await buildAchievementsEmbed( + interaction.guildId, + user, + context.sessionOwnerId, + context.page - 1 + ); + + const pageContext = + getProfileContextFromFooter( + embed.data.footer?.text + ); + + return interaction.update({ + embeds: [embed], + components: + buildProfileButtons( + "achievements", + pageContext?.page ?? 0, + pageContext?.pageCount ?? 1 + ) + }); + } +}; diff --git a/buttons/profile/achievementsRight.js b/buttons/profile/achievementsRight.js new file mode 100644 index 0000000..e35a23d --- /dev/null +++ b/buttons/profile/achievementsRight.js @@ -0,0 +1,54 @@ +const { + buildAchievementsEmbed, + buildProfileButtons, + getProfileContextFromFooter, + requireProfileOwner +} = require( + "../../utils/profileSystem" +); + +module.exports = { + name: + "profileAchievementsRight", + + async execute( + interaction + ) { + const context = + await requireProfileOwner( + interaction + ); + + if (!context) { + return; + } + + const user = + await interaction.client.users.fetch( + context.profileUserId + ); + + const embed = + await buildAchievementsEmbed( + interaction.guildId, + user, + context.sessionOwnerId, + context.page + 1 + ); + + const pageContext = + getProfileContextFromFooter( + embed.data.footer?.text + ); + + return interaction.update({ + embeds: [embed], + components: + buildProfileButtons( + "achievements", + pageContext?.page ?? 0, + pageContext?.pageCount ?? 1 + ) + }); + } +}; diff --git a/buttons/profile/badges.js b/buttons/profile/badges.js index e87af86..75d5251 100644 --- a/buttons/profile/badges.js +++ b/buttons/profile/badges.js @@ -1,22 +1,54 @@ const { buildBadgesEmbed, buildProfileButtons, - getProfileTargetId -} = require("../../utils/profileSystem"); + getProfileContextFromFooter, + requireProfileOwner +} = require( + "../../utils/profileSystem" +); module.exports = { - name: "profileBadges", + name: + "profileBadges", - async execute(interaction) { - const userId = getProfileTargetId(interaction); - if (!userId) return; + async execute( + interaction + ) { + const context = + await requireProfileOwner( + interaction + ); - const user = await interaction.client.users.fetch(userId); - const embed = await buildBadgesEmbed(interaction.guildId, user); + if (!context) { + return; + } + + const user = + await interaction.client.users.fetch( + context.profileUserId + ); + + const embed = + await buildBadgesEmbed( + interaction.guildId, + user, + context.sessionOwnerId, + 0 + ); + + const pageContext = + getProfileContextFromFooter( + embed.data.footer?.text + ); return interaction.update({ embeds: [embed], - components: buildProfileButtons() + components: + buildProfileButtons( + "badges", + pageContext?.page ?? 0, + pageContext?.pageCount ?? 1 + ) }); } }; diff --git a/buttons/profile/badgesLeft.js b/buttons/profile/badgesLeft.js new file mode 100644 index 0000000..ad3f3b7 --- /dev/null +++ b/buttons/profile/badgesLeft.js @@ -0,0 +1,54 @@ +const { + buildBadgesEmbed, + buildProfileButtons, + getProfileContextFromFooter, + requireProfileOwner +} = require( + "../../utils/profileSystem" +); + +module.exports = { + name: + "profileBadgesLeft", + + async execute( + interaction + ) { + const context = + await requireProfileOwner( + interaction + ); + + if (!context) { + return; + } + + const user = + await interaction.client.users.fetch( + context.profileUserId + ); + + const embed = + await buildBadgesEmbed( + interaction.guildId, + user, + context.sessionOwnerId, + context.page - 1 + ); + + const pageContext = + getProfileContextFromFooter( + embed.data.footer?.text + ); + + return interaction.update({ + embeds: [embed], + components: + buildProfileButtons( + "badges", + pageContext?.page ?? 0, + pageContext?.pageCount ?? 1 + ) + }); + } +}; diff --git a/buttons/profile/badgesRight.js b/buttons/profile/badgesRight.js new file mode 100644 index 0000000..8716229 --- /dev/null +++ b/buttons/profile/badgesRight.js @@ -0,0 +1,54 @@ +const { + buildBadgesEmbed, + buildProfileButtons, + getProfileContextFromFooter, + requireProfileOwner +} = require( + "../../utils/profileSystem" +); + +module.exports = { + name: + "profileBadgesRight", + + async execute( + interaction + ) { + const context = + await requireProfileOwner( + interaction + ); + + if (!context) { + return; + } + + const user = + await interaction.client.users.fetch( + context.profileUserId + ); + + const embed = + await buildBadgesEmbed( + interaction.guildId, + user, + context.sessionOwnerId, + context.page + 1 + ); + + const pageContext = + getProfileContextFromFooter( + embed.data.footer?.text + ); + + return interaction.update({ + embeds: [embed], + components: + buildProfileButtons( + "badges", + pageContext?.page ?? 0, + pageContext?.pageCount ?? 1 + ) + }); + } +}; diff --git a/buttons/profile/close.js b/buttons/profile/close.js new file mode 100644 index 0000000..b6ec5cd --- /dev/null +++ b/buttons/profile/close.js @@ -0,0 +1,60 @@ +const { + EmbedBuilder +} = require( + "discord.js" +); + +const { + requireProfileOwner +} = require( + "../../utils/profileSystem" +); + +function delay( + ms +) { + return new Promise( + resolve => + setTimeout( + resolve, + ms + ) + ); +} + +module.exports = { + name: + "profileClose", + + async execute( + interaction + ) { + const context = + await requireProfileOwner( + interaction + ); + + if (!context) { + return; + } + + await interaction.update({ + embeds: [ + new EmbedBuilder() + .setTitle( + "Profile Closed" + ) + .setDescription( + "Closing profile..." + ) + ], + components: [] + }); + + await delay(1200); + + try { + await interaction.deleteReply(); + } catch {} + } +}; diff --git a/buttons/profile/overview.js b/buttons/profile/overview.js index 23049ee..ad34e09 100644 --- a/buttons/profile/overview.js +++ b/buttons/profile/overview.js @@ -1,22 +1,47 @@ const { buildProfileOverview, buildProfileButtons, - getProfileTargetId -} = require("../../utils/profileSystem"); + requireProfileOwner +} = require( + "../../utils/profileSystem" +); module.exports = { - name: "profileOverview", + name: + "profileOverview", - async execute(interaction) { - const userId = getProfileTargetId(interaction); - if (!userId) return; + async execute( + interaction + ) { + const context = + await requireProfileOwner( + interaction + ); - const user = await interaction.client.users.fetch(userId); - const embed = await buildProfileOverview(interaction.guildId, user); + if (!context) { + return; + } + + const user = + await interaction.client.users.fetch( + context.profileUserId + ); + + const embed = + await buildProfileOverview( + interaction.guildId, + user, + context.sessionOwnerId + ); return interaction.update({ embeds: [embed], - components: buildProfileButtons() + components: + buildProfileButtons( + "overview", + 0, + 1 + ) }); } }; diff --git a/buttons/profile/stats.js b/buttons/profile/stats.js index 11f5485..53c56a2 100644 --- a/buttons/profile/stats.js +++ b/buttons/profile/stats.js @@ -1,22 +1,47 @@ const { buildStatsEmbed, buildProfileButtons, - getProfileTargetId -} = require("../../utils/profileSystem"); + requireProfileOwner +} = require( + "../../utils/profileSystem" +); module.exports = { - name: "profileStats", + name: + "profileStats", - async execute(interaction) { - const userId = getProfileTargetId(interaction); - if (!userId) return; + async execute( + interaction + ) { + const context = + await requireProfileOwner( + interaction + ); - const user = await interaction.client.users.fetch(userId); - const embed = await buildStatsEmbed(interaction.guildId, user); + if (!context) { + return; + } + + const user = + await interaction.client.users.fetch( + context.profileUserId + ); + + const embed = + await buildStatsEmbed( + interaction.guildId, + user, + context.sessionOwnerId + ); return interaction.update({ embeds: [embed], - components: buildProfileButtons() + components: + buildProfileButtons( + "stats", + 0, + 1 + ) }); } }; diff --git a/commands/applications/profile.js b/commands/applications/profile.js index 4a7153f..02d4ce4 100644 --- a/commands/applications/profile.js +++ b/commands/applications/profile.js @@ -11,7 +11,6 @@ const { "../../utils/profileSystem" ); - module.exports = { data: new SlashCommandBuilder() @@ -32,32 +31,33 @@ module.exports = { ) ), - async execute( interaction ) { const user = interaction.options.getUser( "user" - ) - ?? + ) ?? interaction.user; + const sessionOwnerId = + interaction.user.id; const embed = await buildProfileOverview( interaction.guildId, - user + user, + sessionOwnerId ); - return interaction.reply({ - embeds: [ - embed - ], - + embeds: [embed], components: - buildProfileButtons() + buildProfileButtons( + "overview", + 0, + 1 + ) }); } -}; \ No newline at end of file +}; diff --git a/utils/profileSystem.js b/utils/profileSystem.js index b371198..f790e9b 100644 --- a/utils/profileSystem.js +++ b/utils/profileSystem.js @@ -654,9 +654,7 @@ async function sendAchievementNotification( achievement.description ].join("\n") ) - ], - flags: - MessageFlags.Ephemeral + ] }); } catch {} } @@ -1020,7 +1018,8 @@ async function tryClaimBadge( async function buildProfileOverview( guildId, - user + user, + sessionOwnerId = user.id ) { const stats = await getProfileStats( @@ -1115,13 +1114,102 @@ async function buildProfileOverview( ) .setFooter({ text: - `profile:${user.id}` + buildProfileFooter( + sessionOwnerId, + user.id, + "overview", + 0, + 1 + ) + }); +} + +async function buildStatsEmbed( + guildId, + user, + sessionOwnerId = user.id +) { + const stats = + await getProfileStats( + user.id, + guildId + ); + + const gameLines = [ + ["🎰 Slots", stats.games.slots], + ["🃏 Blackjack", stats.games.blackjack], + ["🎡 Roulette", stats.games.roulette], + ["🏦 Heist", stats.games.heist] + ].map( + ([name, game]) => [ + `**${name}**`, + `Played ${game?.played ?? 0} • Won ${game?.won ?? 0} • Lost ${game?.lost ?? 0} • Push ${game?.pushed ?? 0}`, + `Net tracked: **+${(game?.cookiesWon ?? 0).toLocaleString()} / -${(game?.cookiesLost ?? 0).toLocaleString()} 🍪**` + ].join("\n") + ); + + return new EmbedBuilder() + .setTitle( + `📊 ${user.globalName ?? user.username}'s Stats` + ) + .setThumbnail( + user.displayAvatarURL({ + size: 256 + }) + ) + .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") + }, + { + name: "🥷 Unique Players", + value: [ + `Stolen from: **${stats.unique?.playersStolenFrom?.length ?? 0}**`, + `Stolen by: **${stats.unique?.playersStolenBy?.length ?? 0}**` + ].join("\n"), + inline: true + }, + { + name: "🔥 Streaks", + value: [ + `Current wins: **${stats.streaks?.currentWin ?? 0}**`, + `Best wins: **${stats.streaks?.bestWin ?? 0}**`, + `Current losses: **${stats.streaks?.currentLoss ?? 0}**`, + `Best losses: **${stats.streaks?.bestLoss ?? 0}**` + ].join("\n"), + inline: true + }, + { + name: "🎮 Per Game", + value: + gameLines.join("\n\n") + } + ) + .setFooter({ + text: + buildProfileFooter( + sessionOwnerId, + user.id, + "stats", + 0, + 1 + ) }); } async function buildBadgesEmbed( guildId, - user + user, + sessionOwnerId = user.id, + page = 0 ) { const { badges } = getProfileCollections(); @@ -1129,6 +1217,9 @@ async function buildBadgesEmbed( const documents = await badges.find({ guildId + }).sort({ + category: 1, + name: 1 }).toArray(); const owned = @@ -1138,117 +1229,177 @@ async function buildBadgesEmbed( user.id ); - const claimedByOthers = - documents.filter( - badge => - badge.ownerId && - badge.ownerId !== - user.id + const pages = []; + + pages.push({ + title: "Owned", + content: + owned.length + ? owned.map( + badge => + formatBadgeLine( + badge, + user.id, + true + ) + ).join("\n\n") + : "*No unique badges yet.*" + }); + + const categoryOrder = [ + "economy", + "stealing", + "slots", + "blackjack", + "roulette", + "heist", + "collector" + ]; + + const categories = + [...new Set( + documents.map( + badge => + badge.category ?? + "other" + ) + )].sort( + (a, b) => { + const ai = + categoryOrder.indexOf(a); + const bi = + categoryOrder.indexOf(b); + + if ( + ai === -1 && + bi === -1 + ) { + return a.localeCompare(b); + } + + if (ai === -1) { + return 1; + } + + if (bi === -1) { + return -1; + } + + return ai - bi; + } ); - const unclaimed = - documents.filter( - badge => - !badge.ownerId + for (const category of categories) { + const categoryBadges = + documents.filter( + badge => + (badge.category ?? "other") === + category + ); + + if (!categoryBadges.length) { + continue; + } + + const chunks = + paginateLines( + categoryBadges.map( + badge => + formatBadgeLine( + badge, + user.id, + false + ) + ), + 3200, + "\n\n" + ); + + for ( + let index = 0; + index < chunks.length; + index++ + ) { + pages.push({ + title: + `${formatCategoryName(category)}${chunks.length > 1 ? ` ${index + 1}/${chunks.length}` : ""}`, + content: + chunks[index] + }); + } + } + + const safePage = + clampPage( + page, + pages.length ); + const selected = + pages[safePage] ?? { + title: "Badges", + content: + "No badges configured." + }; + 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") - : "*No unique badges yet.*", - "", - "### Claimed by Others", - claimedByOthers.length - ? 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") - : "*All unique badges have been claimed.*" + `### ${selected.title}`, + selected.content ].join("\n") ) .setFooter({ text: - `profile:${user.id}` + buildProfileFooter( + sessionOwnerId, + user.id, + "badges", + safePage, + pages.length + ) }); } async function buildAchievementsEmbed( guildId, user, + sessionOwnerId = user.id, page = 0 ) { const { achievements, achievementConfig - } = - getProfileCollections(); - + } = getProfileCollections(); const [ unlockedDoc, definitions - ] = - await Promise.all([ - achievements.findOne({ - userId: - user.id, - - guildId - }), - - achievementConfig - .find({ - active: { - $ne: - false - } - }) - .sort({ - category: - 1, - - target: - 1 - }) - .toArray() - ]); + ] = await Promise.all([ + achievements.findOne({ + userId: user.id, + guildId + }), + achievementConfig + .find({ + active: { + $ne: false + } + }) + .sort({ + category: 1, + target: 1, + name: 1 + }) + .toArray() + ]); const 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; - + unlockedDoc?.unlocked ?? {}; const lines = definitions.map( @@ -1256,171 +1407,83 @@ async function buildAchievementsEmbed( const complete = Boolean( unlocked[ - achievement - .achievementId + achievement.achievementId ] ); - return [ - complete - ? "✅" - : "⬜", - + complete ? "✅" : "⬜", " ", - - achievement.emoji ?? - "🏆", - + achievement.emoji ?? "🏆", " **", - achievement.name, - "** — ", - - achievement.requirement - ?? - achievement.description - ?? + 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" - ) + paginateLines( + lines, + 3400, + "\n" ); - } - - /* - * No achievements configured. - */ - - if ( - pages.length === - 0 - ) { + if (!pages.length) { 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 - ) + clampPage( + page, + pages.length ); - const unlockedCount = definitions.filter( achievement => Boolean( unlocked[ - achievement - .achievementId + achievement.achievementId ] ) ).length; - return new EmbedBuilder() .setTitle( `🏆 ${user.globalName ?? user.username}'s Achievements` ) - .setDescription( - pages[ - safePage - ] + pages[safePage] ) - .addFields({ - name: - "Progress", - + name: "Progress", value: `**${unlockedCount} / ${definitions.length}** achievements unlocked` }) - .setFooter({ text: - `profile:${user.id}:achievements:${safePage}:${pages.length}` + buildProfileFooter( + sessionOwnerId, + user.id, + "achievements", + safePage, + pages.length + ) }); } -function buildProfileButtons() { - return [ +function buildProfileButtons( + section = "overview", + page = 0, + pageCount = 1 +) { + const rows = [ new ActionRowBuilder() .addComponents( new ButtonBuilder() @@ -1434,7 +1497,25 @@ function buildProfileButtons() { "👤" ) .setStyle( - ButtonStyle.Primary + section === "overview" + ? ButtonStyle.Primary + : ButtonStyle.Secondary + ), + + new ButtonBuilder() + .setCustomId( + "profileStats" + ) + .setLabel( + "Stats" + ) + .setEmoji( + "📊" + ) + .setStyle( + section === "stats" + ? ButtonStyle.Primary + : ButtonStyle.Secondary ), new ButtonBuilder() @@ -1448,7 +1529,9 @@ function buildProfileButtons() { "🏅" ) .setStyle( - ButtonStyle.Secondary + section === "badges" + ? ButtonStyle.Primary + : ButtonStyle.Secondary ), new ButtonBuilder() @@ -1462,12 +1545,14 @@ function buildProfileButtons() { "🏆" ) .setStyle( - ButtonStyle.Secondary + section === "achievements" + ? ButtonStyle.Primary + : ButtonStyle.Secondary ), new ButtonBuilder() .setCustomId( - "abort" + "profileClose" ) .setLabel( "Close" @@ -1476,13 +1561,99 @@ function buildProfileButtons() { "✖️" ) .setStyle( - ButtonStyle.Secondary + ButtonStyle.Danger ) ) ]; + + if ( + [ + "badges", + "achievements" + ].includes(section) && + pageCount > 1 + ) { + const prefix = + section === "badges" + ? "profileBadges" + : "profileAchievements"; + + rows.push( + new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId( + `${prefix}Left` + ) + .setLabel( + "Previous" + ) + .setEmoji( + "◀️" + ) + .setDisabled( + page <= 0 + ) + .setStyle( + ButtonStyle.Secondary + ), + + new ButtonBuilder() + .setCustomId( + `${prefix}Page` + ) + .setLabel( + `${page + 1} / ${pageCount}` + ) + .setDisabled( + true + ) + .setStyle( + ButtonStyle.Secondary + ), + + new ButtonBuilder() + .setCustomId( + `${prefix}Right` + ) + .setLabel( + "Next" + ) + .setEmoji( + "▶️" + ) + .setDisabled( + page >= + pageCount - 1 + ) + .setStyle( + ButtonStyle.Secondary + ) + ) + ); + } + + return rows; } -function getProfileTargetId( +function buildProfileFooter( + sessionOwnerId, + profileUserId, + section, + page, + pageCount +) { + return [ + "profile", + sessionOwnerId, + profileUserId, + section, + page, + pageCount + ].join(":"); +} + +function getProfileContext( interaction ) { const footer = @@ -1491,23 +1662,248 @@ function getProfileTargetId( ?.footer ?.text; + return getProfileContextFromFooter( + footer + ); +} + +function getProfileContextFromFooter( + footer +) { const [ type, - userId + sessionOwnerId, + profileUserId, + section = "overview", + rawPage = "0", + rawPageCount = "1" ] = footer?.split(":") ?? []; - return type === "profile" - ? userId - : null; + if ( + type !== "profile" || + !sessionOwnerId || + !profileUserId + ) { + return null; + } + + return { + sessionOwnerId, + profileUserId, + section, + page: + Number(rawPage) || 0, + pageCount: + Math.max( + 1, + Number(rawPageCount) || 1 + ) + }; +} + +function getProfileTargetId( + interaction +) { + return getProfileContext( + interaction + )?.profileUserId ?? null; +} + +async function requireProfileOwner( + interaction +) { + const context = + getProfileContext( + interaction + ); + + if (!context) { + await replyProfileError( + interaction, + "This profile session is no longer valid." + ); + + return null; + } + + if ( + context.sessionOwnerId !== + interaction.user.id + ) { + await replyProfileError( + interaction, + "This isn't your profile session." + ); + + return null; + } + + return context; +} + +async function replyProfileError( + interaction, + content +) { + const payload = { + content, + flags: + MessageFlags.Ephemeral + }; + + if ( + interaction.replied || + interaction.deferred + ) { + return interaction.followUp( + payload + ); + } + + return interaction.reply( + payload + ); +} + +function paginateLines( + lines, + maxLength = 3400, + separator = "\n" +) { + const pages = []; + let current = []; + + for (const line of lines) { + const candidate = + [...current, line].join( + separator + ); + + if ( + candidate.length > + maxLength && + current.length + ) { + pages.push( + current.join( + separator + ) + ); + + current = [line]; + } + + else { + current.push(line); + } + } + + if (current.length) { + pages.push( + current.join( + separator + ) + ); + } + + return pages; +} + +function clampPage( + page, + pageCount +) { + return Math.max( + 0, + Math.min( + Number(page) || 0, + Math.max( + 0, + pageCount - 1 + ) + ) + ); +} + +function formatCategoryName( + category +) { + const labels = { + economy: + "🍪 Economy", + stealing: + "🥷 Stealing", + slots: + "🎰 Slots", + blackjack: + "🃏 Blackjack", + roulette: + "🎡 Roulette", + heist: + "🏦 Heist", + collector: + "🏆 Collector", + other: + "🏅 Other" + }; + + return labels[category] ?? + `🏅 ${category.charAt(0).toUpperCase()}${category.slice(1)}`; +} + +function formatBadgeLine( + badge, + profileUserId, + ownedPage +) { + const emoji = + badge.emoji ?? "🏅"; + + const description = + badge.description ?? + "No description available."; + + if (ownedPage) { + return `${emoji} **${badge.name}**\n${description}`; + } + + let status; + + if ( + badge.ownerId === + profileUserId + ) { + status = "✅ **Owned by this player**"; + } + + else if (badge.ownerId) { + status = `🔒 Claimed by <@${badge.ownerId}>`; + } + + else { + status = "🟢 **Available**"; + } + + return [ + `${emoji} **${badge.name}**`, + description, + status + ].join("\n"); } module.exports = { ensureProfileSystem, trackProfileEvent, getProfileStats, + buildProfileOverview, + buildStatsEmbed, buildBadgesEmbed, buildAchievementsEmbed, buildProfileButtons, - getProfileTargetId + + getProfileContext, + getProfileContextFromFooter, + getProfileTargetId, + requireProfileOwner };