profile update with badges and achievements

This commit is contained in:
DeSqBlocki
2026-08-22 20:27:42 +02:00
parent c630543f9e
commit e437c051b9
7 changed files with 542 additions and 1441 deletions
+22
View File
@@ -0,0 +1,22 @@
const {
buildStatsEmbed,
buildProfileButtons,
getProfileTargetId
} = require("../../utils/profileSystem");
module.exports = {
name: "profileStats",
async execute(interaction) {
const userId = getProfileTargetId(interaction);
if (!userId) return;
const user = await interaction.client.users.fetch(userId);
const embed = await buildStatsEmbed(interaction.guildId, user);
return interaction.update({
embeds: [embed],
components: buildProfileButtons()
});
}
};
+87
View File
@@ -18,6 +18,10 @@ const {
formatPercentage formatPercentage
} = require("../../utils/cookieEconomy"); } = require("../../utils/cookieEconomy");
const {
trackProfileEvent
} = require("../../utils/profileSystem");
const COLLECT_COOLDOWN = const COLLECT_COOLDOWN =
60 * 60; 60 * 60;
@@ -214,6 +218,15 @@ async function collect(interaction) {
amount amount
); );
await trackProfileEvent(
interaction,
userId,
{
type: "cookie_gain",
amount
}
);
const nextCollect = const nextCollect =
await setCooldown( await setCooldown(
@@ -456,6 +469,44 @@ async function steal(interaction) {
} }
await trackProfileEvent(
interaction,
user.id,
{
type: "steal_success",
amount,
targetUserId: target.id
}
);
await trackProfileEvent(
interaction,
user.id,
{
type: "cookie_gain",
amount
}
);
await trackProfileEvent(
interaction,
target.id,
{
type: "stolen_from_me",
amount,
thiefUserId: user.id
}
);
await trackProfileEvent(
interaction,
target.id,
{
type: "cookie_loss",
amount
}
);
const [ const [
yourBalanceAfter, yourBalanceAfter,
targetBalanceAfter targetBalanceAfter
@@ -605,6 +656,24 @@ async function steal(interaction) {
} }
await trackProfileEvent(
interaction,
user.id,
{
type: "cookie_loss",
amount: penalty
}
);
await trackProfileEvent(
interaction,
target.id,
{
type: "cookie_gain",
amount: penalty
}
);
const [ const [
yourBalanceAfter, yourBalanceAfter,
targetBalanceAfter targetBalanceAfter
@@ -851,6 +920,24 @@ async function give(interaction) {
} }
await trackProfileEvent(
interaction,
user.id,
{
type: "cookies_given",
amount
}
);
await trackProfileEvent(
interaction,
target.id,
{
type: "cookie_gain",
amount
}
);
const [ const [
yourBalance, yourBalance,
theirBalance theirBalance
+73
View File
@@ -14,6 +14,10 @@ const {
formatPercentage formatPercentage
} = require("./cookieEconomy"); } = require("./cookieEconomy");
const {
trackProfileEvent
} = require("./profileSystem");
/* /*
* ============================================================ * ============================================================
@@ -394,6 +398,46 @@ async function resolveCookieHeist(
100; 100;
await trackProfileEvent(
interaction,
playerId,
{
type:
"game_result",
game:
"heist",
result:
"win"
}
);
await trackProfileEvent(
interaction,
playerId,
{
type:
"vault_steal",
amount
}
);
await trackProfileEvent(
interaction,
playerId,
{
type:
"cookie_gain",
amount
}
);
const embed = const embed =
new EmbedBuilder() new EmbedBuilder()
.setTitle( .setTitle(
@@ -605,6 +649,35 @@ async function resolveCookieHeist(
100; 100;
await trackProfileEvent(
interaction,
playerId,
{
type:
"game_result",
game:
"heist",
result:
"loss"
}
);
await trackProfileEvent(
interaction,
playerId,
{
type:
"cookie_loss",
amount:
penalty
}
);
const embed = const embed =
new EmbedBuilder() new EmbedBuilder()
.setTitle( .setTitle(
+88 -2
View File
@@ -17,6 +17,10 @@ const {
getUnixTime getUnixTime
} = require("./cookieEconomy"); } = require("./cookieEconomy");
const {
trackProfileEvent
} = require("./profileSystem");
/* /*
* ============================================================ * ============================================================
@@ -1277,7 +1281,7 @@ async function finishBlackjack(
.setFooter({ .setFooter({
text: text:
`crumbBlackjackResult:${game.userId}:${game.bet}` `crumbBlackjackResult:${game.userId}:${game.originalBet ?? game.bet}`
}) })
], ],
@@ -1350,6 +1354,88 @@ async function finishBlackjack(
game.bet; game.bet;
let profileResult;
if (
result ===
"Push"
) {
profileResult =
"push";
}
else if (
[
"Win",
"DealerBust",
"Blackjack"
].includes(
result
)
) {
profileResult =
"win";
}
else {
profileResult =
"loss";
}
await trackProfileEvent(
interaction,
game.userId,
{
type:
"game_result",
game:
"blackjack",
result:
profileResult
}
);
if (
profit >
0
) {
await trackProfileEvent(
interaction,
game.userId,
{
type:
"cookie_gain",
amount:
profit
}
);
}
else if (
profit <
0
) {
await trackProfileEvent(
interaction,
game.userId,
{
type:
"cookie_loss",
amount:
Math.abs(
profit
)
}
);
}
const description = [ const description = [
"**Dealer**", "**Dealer**",
formatCards( formatCards(
@@ -1439,7 +1525,7 @@ async function finishBlackjack(
.setFooter({ .setFooter({
text: text:
`crumbBlackjackResult:${game.userId}:${game.bet}` `crumbBlackjackResult:${game.userId}:${game.originalBet ?? game.bet}`
}); });
+61
View File
@@ -11,6 +11,10 @@ const {
temporaryEphemeralReply temporaryEphemeralReply
} = require("./cookieEconomy"); } = require("./cookieEconomy");
const {
trackProfileEvent
} = require("./profileSystem");
/* /*
* ============================================================ * ============================================================
@@ -852,6 +856,63 @@ async function playRoulette(
bet; bet;
await trackProfileEvent(
interaction,
playerId,
{
type:
"game_result",
game:
"roulette",
result:
profit > 0
? "win"
: profit < 0
? "loss"
: "push"
}
);
if (
profit >
0
) {
await trackProfileEvent(
interaction,
playerId,
{
type:
"cookie_gain",
amount:
profit
}
);
}
else if (
profit <
0
) {
await trackProfileEvent(
interaction,
playerId,
{
type:
"cookie_loss",
amount:
Math.abs(
profit
)
}
);
}
const resultEmbed = const resultEmbed =
new EmbedBuilder() new EmbedBuilder()
.setTitle( .setTitle(
+79 -1416
View File
File diff suppressed because it is too large Load Diff
+132 -23
View File
@@ -21,6 +21,8 @@ const COLLECTIONS = {
achievementConfig: "config_achievements" achievementConfig: "config_achievements"
}; };
let profileSystemInitPromise = null;
const DEFAULT_ACHIEVEMENTS = [ const DEFAULT_ACHIEVEMENTS = [
{ achievementId: "stolen_1000", name: "Light Fingers", description: "Steal 1,000 Cookies from other players.", requirement: "Steal 1,000 Cookies total.", category: "stealing", stat: "economy.stolenTotal", target: 1000, emoji: "🧤" }, { achievementId: "stolen_1000", name: "Light Fingers", description: "Steal 1,000 Cookies from other players.", requirement: "Steal 1,000 Cookies total.", category: "stealing", stat: "economy.stolenTotal", target: 1000, emoji: "🧤" },
{ achievementId: "stolen_10000", name: "Career Criminal", description: "Steal 10,000 Cookies from other players.", requirement: "Steal 10,000 Cookies total.", category: "stealing", stat: "economy.stolenTotal", target: 10000, emoji: "🕵️" }, { achievementId: "stolen_10000", name: "Career Criminal", description: "Steal 10,000 Cookies from other players.", requirement: "Steal 10,000 Cookies total.", category: "stealing", stat: "economy.stolenTotal", target: 10000, emoji: "🕵️" },
@@ -41,7 +43,15 @@ const DEFAULT_ACHIEVEMENTS = [
{ achievementId: "win_streak_3", name: "On a Roll", description: "Win 3 games in a row.", requirement: "Reach a 3-game winning streak.", category: "games", stat: "streaks.bestWin", target: 3, emoji: "🍀" }, { achievementId: "win_streak_3", name: "On a Roll", description: "Win 3 games in a row.", requirement: "Reach a 3-game winning streak.", category: "games", stat: "streaks.bestWin", target: 3, emoji: "🍀" },
{ achievementId: "win_streak_10", name: "Can't Stop Won't Stop", description: "Win 10 games in a row.", requirement: "Reach a 10-game winning streak.", category: "games", stat: "streaks.bestWin", target: 10, emoji: "🔥" }, { achievementId: "win_streak_10", name: "Can't Stop Won't Stop", description: "Win 10 games in a row.", requirement: "Reach a 10-game winning streak.", category: "games", stat: "streaks.bestWin", target: 10, emoji: "🔥" },
{ achievementId: "loss_streak_3", name: "Rough Night", description: "Lose 3 games in a row.", requirement: "Reach a 3-game losing streak.", category: "games", stat: "streaks.bestLoss", target: 3, emoji: "😬" }, { achievementId: "loss_streak_3", name: "Rough Night", description: "Lose 3 games in a row.", requirement: "Reach a 3-game losing streak.", category: "games", stat: "streaks.bestLoss", target: 3, emoji: "😬" },
{ achievementId: "loss_streak_10", name: "Digging Deeper", description: "Lose 10 games in a row.", requirement: "Reach a 10-game losing streak.", category: "games", stat: "streaks.bestLoss", target: 10, emoji: "🕳️" } { achievementId: "loss_streak_10", name: "Digging Deeper", description: "Lose 10 games in a row.", requirement: "Reach a 10-game losing streak.", category: "games", stat: "streaks.bestLoss", target: 10, emoji: "🕳️" },
{ achievementId: "given_10000", name: "Sharing Is Caring", description: "Give 10,000 Cookies to other players.", requirement: "Give 10,000 Cookies to other players total.", category: "economy", stat: "economy.cookiesGivenTotal", target: 10000, emoji: "🎁" },
{ achievementId: "given_50000", name: "Community Baker", description: "Give 50,000 Cookies to other players.", requirement: "Give 50,000 Cookies to other players total.", category: "economy", stat: "economy.cookiesGivenTotal", target: 50000, emoji: "🤝" },
{ achievementId: "games_won_10", name: "House Regular", description: "Win 10 Crumb Saucer games.", requirement: "Win 10 games total.", category: "games", stat: "games.total.won", target: 10, emoji: "🎲" },
{ achievementId: "games_won_50", name: "House Favorite", description: "Win 50 Crumb Saucer games.", requirement: "Win 50 games total.", category: "games", stat: "games.total.won", target: 50, emoji: "🏆" },
{ achievementId: "slots_won_10", name: "Reel Deal", description: "Win 10 Slots spins.", requirement: "Win 10 Slots games.", category: "games", stat: "games.slots.won", target: 10, emoji: "🎰" },
{ achievementId: "blackjack_won_10", name: "Card Counter-ish", description: "Win 10 Blackjack hands.", requirement: "Win 10 Blackjack games.", category: "games", stat: "games.blackjack.won", target: 10, emoji: "🃏" },
{ achievementId: "roulette_won_10", name: "Wheel Watcher", description: "Win 10 Roulette bets.", requirement: "Win 10 Roulette games.", category: "games", stat: "games.roulette.won", target: 10, emoji: "🎡" },
{ achievementId: "heist_won_10", name: "Professional", description: "Complete 10 successful Vault Heists.", requirement: "Win 10 Vault Heists.", category: "games", stat: "games.heist.won", target: 10, emoji: "🏦" }
]; ];
const DEFAULT_BADGES = [ const DEFAULT_BADGES = [
@@ -63,22 +73,34 @@ function getProfileCollections() {
} }
async function ensureProfileSystem() { async function ensureProfileSystem() {
const { stats, achievements, badges, achievementConfig } = getProfileCollections(); if (profileSystemInitPromise) return profileSystemInitPromise;
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 });
for (const achievement of DEFAULT_ACHIEVEMENTS) { profileSystemInitPromise = (async () => {
await achievementConfig.updateOne( const { stats, achievements, badges, achievementConfig } = getProfileCollections();
{ achievementId: achievement.achievementId }, await stats.createIndex({ userId: 1, guildId: 1 }, { unique: true });
{ $setOnInsert: achievement }, await achievements.createIndex({ userId: 1, guildId: 1 }, { unique: true });
{ upsert: true } await badges.createIndex({ guildId: 1, badgeId: 1 }, { unique: true });
); await achievementConfig.createIndex({ achievementId: 1 }, { unique: true });
for (const achievement of DEFAULT_ACHIEVEMENTS) {
await achievementConfig.updateOne(
{ achievementId: achievement.achievementId },
{ $setOnInsert: achievement },
{ upsert: true }
);
}
})();
try {
return await profileSystemInitPromise;
} catch (error) {
profileSystemInitPromise = null;
throw error;
} }
} }
async function ensureGuildBadges(guildId) { async function ensureGuildBadges(guildId) {
await ensureProfileSystem();
const { badges } = getProfileCollections(); const { badges } = getProfileCollections();
for (const badge of DEFAULT_BADGES) { for (const badge of DEFAULT_BADGES) {
await badges.updateOne( await badges.updateOne(
@@ -145,6 +167,7 @@ async function getProfileStats(userId, guildId) {
} }
async function trackProfileEvent(interaction, userId, event) { async function trackProfileEvent(interaction, userId, event) {
await ensureProfileSystem();
const guildId = interaction.guildId; const guildId = interaction.guildId;
if (!guildId || !userId || !event?.type) return; if (!guildId || !userId || !event?.type) return;
@@ -190,7 +213,7 @@ async function trackProfileEvent(interaction, userId, event) {
} }
const updatedStats = await getProfileStats(userId, guildId); const updatedStats = await getProfileStats(userId, guildId);
await evaluateAchievements(interaction, userId, updatedStats); await evaluateAchievements(interaction, userId, updatedStats, event.type);
await evaluateBadges(interaction, userId, updatedStats); await evaluateBadges(interaction, userId, updatedStats);
return updatedStats; return updatedStats;
} }
@@ -244,12 +267,15 @@ async function trackGameResult(userId, guildId, event) {
); );
} }
async function evaluateAchievements(interaction, userId, statsDocument) { async function evaluateAchievements(interaction, userId, statsDocument, eventType = null) {
const { achievements, achievementConfig } = getProfileCollections(); const { achievements, achievementConfig } = getProfileCollections();
const guildId = interaction.guildId; const guildId = interaction.guildId;
await ensureAchievementDocument(userId, guildId); await ensureAchievementDocument(userId, guildId);
const definitions = await achievementConfig.find({}).toArray(); const relevantStats = getRelevantAchievementStats(eventType);
const definitions = await achievementConfig.find(
relevantStats ? { stat: { $in: relevantStats } } : {}
).toArray();
const unlockedDoc = await achievements.findOne({ userId, guildId }); const unlockedDoc = await achievements.findOne({ userId, guildId });
const unlocked = unlockedDoc?.unlocked ?? {}; const unlocked = unlockedDoc?.unlocked ?? {};
@@ -265,7 +291,7 @@ async function evaluateAchievements(interaction, userId, statsDocument) {
); );
if (result.modifiedCount === 1) { if (result.modifiedCount === 1) {
await sendAchievementNotification(interaction, definition); await sendAchievementNotification(interaction, userId, definition);
} }
} }
} }
@@ -314,23 +340,38 @@ async function tryClaimBadge(interaction, userId, badgeId) {
} catch {} } catch {}
} }
async function sendAchievementNotification(interaction, achievement) { async function sendAchievementNotification(interaction, userId, achievement) {
try { try {
await interaction.followUp({ await interaction.channel.send({
embeds: [ embeds: [
new EmbedBuilder() new EmbedBuilder()
.setTitle("🏆 Achievement Unlocked!") .setTitle("🏆 Achievement Unlocked!")
.setDescription([ .setDescription([
`<@${userId}>`,
"",
`${achievement.emoji} **${achievement.name}**`, `${achievement.emoji} **${achievement.name}**`,
"", "",
achievement.description achievement.description
].join("\n")) ].join("\n"))
], ]
flags: MessageFlags.Ephemeral
}); });
} catch {} } catch {}
} }
function getRelevantAchievementStats(eventType) {
switch (eventType) {
case "cookie_gain": return ["economy.cookiesObtainedTotal"];
case "cookie_loss": return ["economy.cookiesLostTotal"];
case "cookies_given": return ["economy.cookiesGivenTotal"];
case "steal_success": return ["economy.stolenTotal", "unique.playersStolenFrom"];
case "stolen_from_me": return ["economy.stolenFromMeTotal", "unique.playersStolenBy"];
case "vault_steal": return ["economy.vaultStolenTotal"];
case "game_result": return ["streaks.bestWin", "streaks.bestLoss", "games.total.won", "games.slots.won", "games.blackjack.won", "games.roulette.won", "games.heist.won"];
default: return null;
}
}
function readStatValue(document, definition) { function readStatValue(document, definition) {
const value = definition.stat const value = definition.stat
.split(".") .split(".")
@@ -344,6 +385,7 @@ function readStatValue(document, definition) {
} }
async function buildProfileOverview(guildId, user) { async function buildProfileOverview(guildId, user) {
await ensureProfileSystem();
const stats = await getProfileStats(user.id, guildId); const stats = await getProfileStats(user.id, guildId);
const balance = await getCookieBalance(user.id); const balance = await getCookieBalance(user.id);
const { achievements, badges, achievementConfig } = getProfileCollections(); const { achievements, badges, achievementConfig } = getProfileCollections();
@@ -386,6 +428,7 @@ async function buildProfileOverview(guildId, user) {
} }
async function buildBadgesEmbed(guildId, user) { async function buildBadgesEmbed(guildId, user) {
await ensureProfileSystem();
await ensureGuildBadges(guildId); await ensureGuildBadges(guildId);
const { badges } = getProfileCollections(); const { badges } = getProfileCollections();
const documents = await badges.find({ guildId }).toArray(); const documents = await badges.find({ guildId }).toArray();
@@ -416,6 +459,7 @@ async function buildBadgesEmbed(guildId, user) {
} }
async function buildAchievementsEmbed(guildId, user) { async function buildAchievementsEmbed(guildId, user) {
await ensureProfileSystem();
const { achievements, achievementConfig } = getProfileCollections(); const { achievements, achievementConfig } = getProfileCollections();
const [unlockedDoc, definitions] = await Promise.all([ const [unlockedDoc, definitions] = await Promise.all([
@@ -424,9 +468,16 @@ async function buildAchievementsEmbed(guildId, user) {
]); ]);
const unlocked = unlockedDoc?.unlocked ?? {}; const unlocked = unlockedDoc?.unlocked ?? {};
const lines = definitions.map(a => const stats = await getProfileStats(user.id, guildId);
`${unlocked[a.achievementId] ? "✅" : "⬜"} ${a.emoji} **${a.name}** — ${a.requirement}` const lines = definitions.map(a => {
); const complete = Boolean(unlocked[a.achievementId]);
const current = readStatValue(stats, a);
const progress = complete
? "Unlocked"
: `${Math.min(current, a.target).toLocaleString()} / ${a.target.toLocaleString()}`;
return `${complete ? "✅" : "⬜"} ${a.emoji} **${a.name}** — ${a.requirement}\n↳ ${progress}`;
});
return new EmbedBuilder() return new EmbedBuilder()
.setTitle(`🏆 ${user.globalName ?? user.username}'s Achievements`) .setTitle(`🏆 ${user.globalName ?? user.username}'s Achievements`)
@@ -434,10 +485,67 @@ async function buildAchievementsEmbed(guildId, user) {
.setFooter({ text: `profile:${user.id}` }); .setFooter({ text: `profile:${user.id}` });
} }
async function buildStatsEmbed(guildId, user) {
await ensureProfileSystem();
const stats = await getProfileStats(user.id, guildId);
const e = stats.economy;
const g = stats.games;
return new EmbedBuilder()
.setTitle(`📊 ${user.globalName ?? user.username}'s Statistics`)
.setThumbnail(user.displayAvatarURL({ size: 256 }))
.addFields(
{
name: "🍪 Economy",
value: [
`Obtained: **${e.cookiesObtainedTotal.toLocaleString()}**`,
`Lost: **${e.cookiesLostTotal.toLocaleString()}**`,
`Given: **${e.cookiesGivenTotal.toLocaleString()}**`,
`Stolen: **${e.stolenTotal.toLocaleString()}**`,
`Stolen from you: **${e.stolenFromMeTotal.toLocaleString()}**`,
`Bot Vault stolen: **${e.vaultStolenTotal.toLocaleString()}**`
].join("\n")
},
{
name: "🥷 Theft",
value: [
`Unique victims: **${stats.unique.playersStolenFrom.length}**`,
`Unique thieves: **${stats.unique.playersStolenBy.length}**`
].join("\n"),
inline: true
},
{
name: "🔥 Streaks",
value: [
`Current wins: **${stats.streaks.currentWin}**`,
`Best wins: **${stats.streaks.bestWin}**`,
`Current losses: **${stats.streaks.currentLoss}**`,
`Best losses: **${stats.streaks.bestLoss}**`
].join("\n"),
inline: true
},
{ name: "🎰 Slots", value: formatGameStats(g.slots), inline: true },
{ name: "🃏 Blackjack", value: formatGameStats(g.blackjack), inline: true },
{ name: "🎡 Roulette", value: formatGameStats(g.roulette), inline: true },
{ name: "🏦 Heist", value: formatGameStats(g.heist), inline: true }
)
.setFooter({ text: `profile:${user.id}` });
}
function formatGameStats(game) {
return [
`Played: **${game?.played ?? 0}**`,
`Won: **${game?.won ?? 0}**`,
`Lost: **${game?.lost ?? 0}**`,
`Pushes: **${game?.pushed ?? 0}**`
].join("\n");
}
function buildProfileButtons() { function buildProfileButtons() {
return [ return [
new ActionRowBuilder().addComponents( new ActionRowBuilder().addComponents(
new ButtonBuilder().setCustomId("profileOverview").setLabel("Overview").setEmoji("👤").setStyle(ButtonStyle.Primary), new ButtonBuilder().setCustomId("profileOverview").setLabel("Overview").setEmoji("👤").setStyle(ButtonStyle.Primary),
new ButtonBuilder().setCustomId("profileStats").setLabel("Stats").setEmoji("📊").setStyle(ButtonStyle.Secondary),
new ButtonBuilder().setCustomId("profileBadges").setLabel("Badges").setEmoji("🏅").setStyle(ButtonStyle.Secondary), new ButtonBuilder().setCustomId("profileBadges").setLabel("Badges").setEmoji("🏅").setStyle(ButtonStyle.Secondary),
new ButtonBuilder().setCustomId("profileAchievements").setLabel("Achievements").setEmoji("🏆").setStyle(ButtonStyle.Secondary), new ButtonBuilder().setCustomId("profileAchievements").setLabel("Achievements").setEmoji("🏆").setStyle(ButtonStyle.Secondary),
new ButtonBuilder().setCustomId("abort").setLabel("Close").setEmoji("✖️").setStyle(ButtonStyle.Secondary) new ButtonBuilder().setCustomId("abort").setLabel("Close").setEmoji("✖️").setStyle(ButtonStyle.Secondary)
@@ -457,6 +565,7 @@ module.exports = {
trackProfileEvent, trackProfileEvent,
getProfileStats, getProfileStats,
buildProfileOverview, buildProfileOverview,
buildStatsEmbed,
buildBadgesEmbed, buildBadgesEmbed,
buildAchievementsEmbed, buildAchievementsEmbed,
buildProfileButtons, buildProfileButtons,