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

3236 lines
65 KiB
JavaScript

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