Files
ShaiBot/utils/profileSystem.js
T
2026-08-23 00:43:43 +02:00

2213 lines
48 KiB
JavaScript

const {
EmbedBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
MessageFlags
} = require("discord.js");
const { mClient } = require("../index");
const { getCookieBalance, getUnixTime } = require("./cookieEconomy");
const DATABASE_NAME = process.env.M_DB;
const COLLECTIONS = {
stats: "stats_profiles",
achievements: "items_achievements",
badges: "guild_badges",
achievementConfig: "config_achievements"
};
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)
};
}
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 }
);
}
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,
currentRunItBackStreak: 0,
bestRunItBackStreak: 0
},
blackjack: {
played: 0,
won: 0,
lost: 0,
pushed: 0,
cookiesWon: 0,
cookiesLost: 0,
currentRunItBackStreak: 0,
bestRunItBackStreak: 0
},
roulette: {
played: 0,
won: 0,
lost: 0,
pushed: 0,
cookiesWon: 0,
cookiesLost: 0,
currentRunItBackStreak: 0,
bestRunItBackStreak: 0
},
heist: {
played: 0,
won: 0,
lost: 0,
pushed: 0,
cookiesWon: 0,
cookiesLost: 0
}
},
streaks: {
currentWin: 0,
bestWin: 0,
currentLoss: 0,
bestLoss: 0
},
special: {
allInRunItBack: 0
}
};
}
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 }
);
}
async function getProfileStats(userId, guildId) {
const { stats } = getProfileCollections();
return await stats.findOne({
userId,
guildId
}) ?? getDefaultStats(
userId,
guildId
);
}
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":
event.amount
}
}
);
break;
case "cookie_loss":
await stats.updateOne(
{ userId, guildId },
{
$inc: {
"economy.cookiesLostTotal":
event.amount
}
}
);
break;
case "cookies_given":
await stats.updateOne(
{ userId, guildId },
{
$inc: {
"economy.cookiesGivenTotal":
event.amount
}
}
);
break;
case "steal_success":
await stats.updateOne(
{ userId, guildId },
{
$inc: {
"economy.stolenTotal":
event.amount
},
$addToSet: {
"unique.playersStolenFrom":
event.targetUserId
}
}
);
break;
case "stolen_from_me":
await stats.updateOne(
{ userId, guildId },
{
$inc: {
"economy.stolenFromMeTotal":
event.amount
},
$addToSet: {
"unique.playersStolenBy":
event.thiefUserId
}
}
);
break;
case "vault_steal":
await stats.updateOne(
{ userId, guildId },
{
$inc: {
"economy.vaultStolenTotal":
event.amount
}
}
);
break;
case "game_result":
await trackGameResult(
userId,
guildId,
event
);
break;
case "game_start":
await trackGameStart(
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;
}
async function recordGameOutcome(
interaction,
userId,
{
game,
result,
profit = 0,
...metadata
}
) {
return trackProfileEvent(
interaction,
userId,
{
type: "game_result",
game,
result,
profit,
...metadata
}
);
}
async function trackGameStart(
userId,
guildId,
event
) {
const { stats } = getProfileCollections();
const game = String(
event.game ?? ""
).toLowerCase();
if (!["slots", "roulette", "blackjack"].includes(game)) {
return;
}
const source = String(
event.source ?? "normal"
).toLowerCase();
const currentPath =
`games.${game}.currentRunItBackStreak`;
const bestPath =
`games.${game}.bestRunItBackStreak`;
if (source !== "run_it_back") {
await stats.updateOne(
{ userId, guildId },
{
$set: {
[currentPath]: 0
}
}
);
return;
}
const updated = await stats.findOneAndUpdate(
{ userId, guildId },
{
$inc: {
[currentPath]: 1
}
},
{
returnDocument: "after"
}
);
const currentStreak =
updated?.games?.[game]?.currentRunItBackStreak ?? 0;
const set = {};
const previousBet = Number(
event.previousBet ?? 0
);
const currentBet = Number(
event.bet ?? 0
);
const chainedAllIn =
event.previousAllIn === true &&
previousBet > 0 &&
currentBet === previousBet;
if (chainedAllIn) {
set["special.allInRunItBack"] = 1;
}
await stats.updateOne(
{ userId, guildId },
{
$max: {
[bestPath]: currentStreak
},
...(Object.keys(set).length
? { $set: set }
: {})
}
);
}
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;
increment[
"economy.cookiesObtainedTotal"
] = profit;
}
else if (profit < 0) {
increment[
`games.${game}.cookiesLost`
] = Math.abs(profit);
increment[
"economy.cookiesLostTotal"
] = 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
}
}
);
}
async function evaluateAchievements(
interaction,
userId,
statsDocument
) {
const guildId = interaction.guildId;
const {
achievements,
achievementConfig
} = getProfileCollections();
await ensureAchievementDocument(
userId,
guildId
);
const definitions =
await achievementConfig
.find({
active: {
$ne: false
}
})
.toArray();
let unlockedDoc =
await achievements.findOne({
userId,
guildId
});
let unlocked =
unlockedDoc?.unlocked ?? {};
const normalDefinitions =
definitions.filter(
definition =>
definition.measure !==
"achievementPercentage"
);
for (const definition of normalDefinitions) {
if (unlocked[definition.achievementId]) {
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();
}
}
unlockedDoc =
await achievements.findOne({
userId,
guildId
});
unlocked =
unlockedDoc?.unlocked ?? {};
const collectorDefinitions =
definitions.filter(
definition =>
definition.measure ===
"achievementPercentage"
);
const collectibleDefinitions =
definitions.filter(
definition =>
definition.measure !==
"achievementPercentage" &&
Boolean(definition.stat)
);
const totalCollectible =
collectibleDefinitions.length;
const unlockedCollectible =
collectibleDefinitions.filter(
definition =>
Boolean(
unlocked[
definition.achievementId
]
)
).length;
const achievementPercentage =
totalCollectible > 0
? unlockedCollectible /
totalCollectible
: 0;
for (const definition of collectorDefinitions) {
if (unlocked[definition.achievementId]) {
continue;
}
if (
achievementPercentage <
Number(definition.target)
) {
continue;
}
const unlockedNow =
await unlockAchievement(
interaction,
userId,
guildId,
definition
);
if (unlockedNow) {
unlocked[
definition.achievementId
] = getUnixTime();
}
}
}
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;
}
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);
}
async function sendAchievementNotification(
interaction,
achievement
) {
try {
await interaction.followUp({
embeds: [
new EmbedBuilder()
.setTitle(
"🏆 Achievement Unlocked!"
)
.setDescription(
[
`${achievement.emoji} **${achievement.name}**`,
"",
achievement.description
].join("\n")
)
]
});
} catch {}
}
async function evaluateBadges(
interaction,
userId,
statsDocument,
event = {}
) {
const guildId = interaction.guildId;
if (!guildId) {
return;
}
const balance =
await getCookieBalance(
userId
);
if (balance >= 100000) {
await tryClaimBadge(
interaction,
userId,
"first_100k_cookies"
);
}
if (balance >= 1000000) {
await tryClaimBadge(
interaction,
userId,
"first_1m_cookies"
);
}
if (
(
statsDocument
?.economy
?.vaultStolenTotal ?? 0
) >= 100000
) {
await tryClaimBadge(
interaction,
userId,
"first_100k_vault_stolen"
);
}
const uniquePlayersStolenFrom =
statsDocument
?.unique
?.playersStolenFrom ?? [];
const uniqueStealCount =
uniquePlayersStolenFrom.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"
);
}
if (
event.type === "game_result" &&
event.game === "slots"
) {
const tripletBadges = {
"🍋": "first_slots_triple_lemon",
"🍒": "first_slots_triple_cherry",
"🍪": "first_slots_triple_cookie",
"⭐": "first_slots_triple_star",
"💎": "first_slots_jackpot"
};
const badgeId =
tripletBadges[event.triplet];
if (badgeId) {
await tryClaimBadge(
interaction,
userId,
badgeId
);
}
}
if (
event.type ===
"game_result" &&
event.game ===
"blackjack" &&
event.natural ===
true &&
event.result ===
"win"
) {
await tryClaimBadge(
interaction,
userId,
"first_blackjack_natural"
);
}
if (
event.type ===
"game_result" &&
event.game ===
"blackjack" &&
event.allIn ===
true &&
event.result ===
"win"
) {
await tryClaimBadge(
interaction,
userId,
"first_blackjack_allin_win"
);
}
if (
event.type ===
"game_result" &&
event.game ===
"roulette" &&
event.zero ===
true &&
event.result ===
"win"
) {
await tryClaimBadge(
interaction,
userId,
"first_roulette_zero"
);
}
if (
event.type ===
"game_result" &&
event.game ===
"roulette" &&
event.allIn ===
true &&
event.result ===
"win"
) {
await tryClaimBadge(
interaction,
userId,
"first_roulette_allin_win"
);
}
if (
event.type === "game_start" &&
event.source === "run_it_back" &&
event.previousAllIn === true &&
Number(event.previousBet) > 0 &&
Number(event.bet) === Number(event.previousBet)
) {
await tryClaimBadge(
interaction,
userId,
"first_all_in_run_it_back"
);
}
await evaluateCollectorBadges(
interaction,
userId
);
}
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()
).filter(achievement =>
Boolean(achievement.stat)
);
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
}
);
}
}
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.",
"",
"*This badge can no longer be earned by another player.*"
];
if (
badge.metadata
?.unlockedAchievements !==
undefined &&
badge.metadata
?.availableAchievements !==
undefined
) {
description.splice(
description.length - 1,
0,
`Claimed at **${badge.metadata.unlockedAchievements}/${badge.metadata.availableAchievements} achievements**.`,
""
);
}
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;
}
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 [
achievementDoc,
achievementCount,
ownedBadges
] = await Promise.all([
achievements.findOne({
userId: user.id,
guildId
}),
achievementConfig.countDocuments({
active: {
$ne: false
}
}),
badges.find({
guildId,
ownerId: user.id
}).toArray()
]);
const unlockedCount =
Object.keys(
achievementDoc?.unlocked ?? {}
).length;
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}`,
inline: true
},
{
name: "🏅 Unique Badges",
value:
`${ownedBadges.length}`,
inline: true
},
{
name: "🎮 Games",
value: [
`Played: **${stats.games.total.played}**`,
`Won: **${stats.games.total.won}**`,
`Lost: **${stats.games.total.lost}**`,
`Pushes: **${stats.games.total.pushed}**`
].join("\n"),
inline: true
},
{
name: "🔥 Best Streaks",
value: [
`Wins: **${stats.streaks.bestWin}**`,
`Losses: **${stats.streaks.bestLoss}**`
].join("\n"),
inline: true
}
)
.setFooter({
text:
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 = {}]) => {
const played =
Number(game.played ?? 0);
const won =
Number(game.won ?? 0);
const lost =
Number(game.lost ?? 0);
const pushed =
Number(game.pushed ?? 0);
const cookiesWon =
Number(game.cookiesWon ?? 0);
const cookiesLost =
Number(game.cookiesLost ?? 0);
const decidedGames =
won + lost;
const winRate =
decidedGames > 0
? (won / decidedGames) * 100
: 0;
const net =
cookiesWon - cookiesLost;
return [
`**${name}**`,
`Played **${played}** • W **${won}** • L **${lost}** • Push **${pushed}**`,
`Win rate: **${winRate.toFixed(1)}%**`,
`Won **${cookiesWon.toLocaleString()} 🍪** • Lost **${cookiesLost.toLocaleString()} 🍪**`,
`Net: **${net >= 0 ? "+" : ""}${net.toLocaleString()} 🍪**`
].join("\n");
});
const economy =
stats.economy ?? {};
const unique =
stats.unique ?? {};
const streaks =
stats.streaks ?? {};
return new EmbedBuilder()
.setTitle(
`📊 ${user.globalName ?? user.username}'s Stats`
)
.setThumbnail(
user.displayAvatarURL({
size: 256
})
)
.addFields(
{
name: "🍪 Economy",
value: [
`Obtained: **${Number(economy.cookiesObtainedTotal ?? 0).toLocaleString()} 🍪**`,
`Lost: **${Number(economy.cookiesLostTotal ?? 0).toLocaleString()} 🍪**`,
`Given: **${Number(economy.cookiesGivenTotal ?? 0).toLocaleString()} 🍪**`,
`Stolen: **${Number(economy.stolenTotal ?? 0).toLocaleString()} 🍪**`,
`Stolen from you: **${Number(economy.stolenFromMeTotal ?? 0).toLocaleString()} 🍪**`,
`Vault stolen: **${Number(economy.vaultStolenTotal ?? 0).toLocaleString()} 🍪**`
].join("\n")
},
{
name: "🥷 Unique Players",
value: [
`Stolen from: **${unique.playersStolenFrom?.length ?? 0}**`,
`Stolen by: **${unique.playersStolenBy?.length ?? 0}**`
].join("\n"),
inline: true
},
{
name: "🔥 Streaks",
value: [
`Current wins: **${streaks.currentWin ?? 0}**`,
`Best wins: **${streaks.bestWin ?? 0}**`,
`Current losses: **${streaks.currentLoss ?? 0}**`,
`Best losses: **${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,
sessionOwnerId = user.id,
page = 0
) {
const { badges } =
getProfileCollections();
const documents =
await badges.find({
guildId
}).sort({
category: 1,
name: 1
}).toArray();
const owned =
documents.filter(
badge =>
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;
}
);
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(
[
`### ${selected.title}`,
selected.content
].join("\n")
)
.setFooter({
text:
buildProfileFooter(
sessionOwnerId,
user.id,
"badges",
safePage,
pages.length
)
});
}
async function buildAchievementsEmbed(
guildId,
user,
sessionOwnerId = user.id,
page = 0
) {
const {
achievements,
achievementConfig
} = getProfileCollections();
const [
unlockedDoc,
definitions,
statsDocument
] = await Promise.all([
achievements.findOne({
userId: user.id,
guildId
}),
achievementConfig
.find({
active: {
$ne: false
}
})
.sort({
category: 1,
target: 1,
name: 1
})
.toArray(),
getProfileStats(
user.id,
guildId
)
]);
const unlocked =
unlockedDoc?.unlocked ?? {};
const collectibleDefinitions =
definitions.filter(
definition =>
definition.measure !==
"achievementPercentage"
);
const unlockedCollectible =
collectibleDefinitions.filter(
definition =>
Boolean(
unlocked[
definition.achievementId
]
)
).length;
const collectorPercentage =
collectibleDefinitions.length > 0
? unlockedCollectible /
collectibleDefinitions.length
: 0;
const lines = [];
let lastCategory = null;
for (const achievement of definitions) {
const complete =
Boolean(
unlocked[
achievement.achievementId
]
);
const category =
achievement.category ??
"other";
if (category !== lastCategory) {
if (lines.length) {
lines.push("");
}
lines.push(
`### ${formatCategoryName(category)}`
);
lastCategory = category;
}
if (
achievement.secret === true &&
!complete
) {
lines.push(
"⬜ ❓ **???** — Hidden achievement"
);
continue;
}
let currentValue;
let targetValue =
Number(achievement.target ?? 0);
let progressText = "";
if (
achievement.measure ===
"achievementPercentage"
) {
currentValue =
collectorPercentage;
progressText =
` • **${Math.min(100, currentValue * 100).toFixed(0)}% / ${Math.min(100, targetValue * 100).toFixed(0)}%**`;
}
else if (achievement.stat) {
currentValue =
readStatValue(
statsDocument,
achievement
);
progressText =
` • **${Number(currentValue).toLocaleString()} / ${targetValue.toLocaleString()}**`;
}
const requirement =
achievement.requirement ??
achievement.description ??
"Requirement unavailable.";
lines.push(
[
complete ? "✅" : "⬜",
" ",
achievement.emoji ?? "🏆",
" **",
achievement.name,
"** — ",
requirement,
complete
? ""
: progressText
].join("")
);
}
const pages =
paginateLines(
lines,
3400,
"\n"
);
if (!pages.length) {
pages.push(
"No achievements configured."
);
}
const safePage =
clampPage(
page,
pages.length
);
const unlockedCount =
definitions.filter(
achievement =>
Boolean(
unlocked[
achievement.achievementId
]
)
).length;
const completion =
definitions.length > 0
? (unlockedCount / definitions.length) * 100
: 0;
return new EmbedBuilder()
.setTitle(
`🏆 ${user.globalName ?? user.username}'s Achievements`
)
.setDescription(
pages[safePage]
)
.addFields({
name: "Progress",
value:
`**${unlockedCount} / ${definitions.length}** unlocked • **${completion.toFixed(1)}%** complete`
})
.setFooter({
text:
buildProfileFooter(
sessionOwnerId,
user.id,
"achievements",
safePage,
pages.length
)
});
}
function buildProfileButtons(
section = "overview",
page = 0,
pageCount = 1
) {
const rows = [
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(
"profileOverview"
)
.setLabel(
"Overview"
)
.setEmoji(
"👤"
)
.setStyle(
section === "overview"
? ButtonStyle.Primary
: ButtonStyle.Secondary
),
new ButtonBuilder()
.setCustomId(
"profileStats"
)
.setLabel(
"Stats"
)
.setEmoji(
"📊"
)
.setStyle(
section === "stats"
? ButtonStyle.Primary
: ButtonStyle.Secondary
),
new ButtonBuilder()
.setCustomId(
"profileBadges"
)
.setLabel(
"Badges"
)
.setEmoji(
"🏅"
)
.setStyle(
section === "badges"
? ButtonStyle.Primary
: ButtonStyle.Secondary
),
new ButtonBuilder()
.setCustomId(
"profileAchievements"
)
.setLabel(
"Achievements"
)
.setEmoji(
"🏆"
)
.setStyle(
section === "achievements"
? ButtonStyle.Primary
: ButtonStyle.Secondary
),
new ButtonBuilder()
.setCustomId(
"profileClose"
)
.setLabel(
"Close"
)
.setEmoji(
"✖️"
)
.setStyle(
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 buildProfileFooter(
sessionOwnerId,
profileUserId,
section,
page,
pageCount
) {
return [
"profile",
sessionOwnerId,
profileUserId,
section,
page,
pageCount
].join(":");
}
function getProfileContext(
interaction
) {
const footer =
interaction.message
?.embeds?.[0]
?.footer
?.text;
return getProfileContextFromFooter(
footer
);
}
function getProfileContextFromFooter(
footer
) {
const [
type,
sessionOwnerId,
profileUserId,
section = "overview",
rawPage = "0",
rawPageCount = "1"
] = footer?.split(":") ?? [];
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,
recordGameOutcome,
getProfileStats,
getProfileCollections,
getDefaultStats,
readStatValue,
buildProfileOverview,
buildStatsEmbed,
buildBadgesEmbed,
buildAchievementsEmbed,
buildProfileButtons,
getProfileContext,
getProfileContextFromFooter,
getProfileTargetId,
requireProfileOwner
};