updated badges
This commit is contained in:
@@ -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
|
||||
)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
)
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
)
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
)
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
)
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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 {}
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
)
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
+609
-213
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user