updated badges

This commit is contained in:
DeSqBlocki
2026-08-22 22:46:40 +02:00
parent 9e0870c1ed
commit 08d1a8c5cd
11 changed files with 1047 additions and 261 deletions
+41 -9
View File
@@ -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
)
});
}
};
+54
View File
@@ -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
)
});
}
};
+54
View File
@@ -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
)
});
}
};
+41 -9
View File
@@ -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
)
});
}
};
+54
View File
@@ -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
)
});
}
};
+54
View File
@@ -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
)
});
}
};
+60
View File
@@ -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 {}
}
};
+34 -9
View File
@@ -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
)
});
}
};
+34 -9
View File
@@ -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
)
});
}
};
+12 -12
View File
@@ -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
View File
@@ -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
};