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
} = require("../../utils/cookieEconomy");
const {
trackProfileEvent
} = require("../../utils/profileSystem");
const COLLECT_COOLDOWN =
60 * 60;
@@ -214,6 +218,15 @@ async function collect(interaction) {
amount
);
await trackProfileEvent(
interaction,
userId,
{
type: "cookie_gain",
amount
}
);
const nextCollect =
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 [
yourBalanceAfter,
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 [
yourBalanceAfter,
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 [
yourBalance,
theirBalance
+73
View File
@@ -14,6 +14,10 @@ const {
formatPercentage
} = require("./cookieEconomy");
const {
trackProfileEvent
} = require("./profileSystem");
/*
* ============================================================
@@ -394,6 +398,46 @@ async function resolveCookieHeist(
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 =
new EmbedBuilder()
.setTitle(
@@ -605,6 +649,35 @@ async function resolveCookieHeist(
100;
await trackProfileEvent(
interaction,
playerId,
{
type:
"game_result",
game:
"heist",
result:
"loss"
}
);
await trackProfileEvent(
interaction,
playerId,
{
type:
"cookie_loss",
amount:
penalty
}
);
const embed =
new EmbedBuilder()
.setTitle(
+88 -2
View File
@@ -17,6 +17,10 @@ const {
getUnixTime
} = require("./cookieEconomy");
const {
trackProfileEvent
} = require("./profileSystem");
/*
* ============================================================
@@ -1277,7 +1281,7 @@ async function finishBlackjack(
.setFooter({
text:
`crumbBlackjackResult:${game.userId}:${game.bet}`
`crumbBlackjackResult:${game.userId}:${game.originalBet ?? game.bet}`
})
],
@@ -1350,6 +1354,88 @@ async function finishBlackjack(
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 = [
"**Dealer**",
formatCards(
@@ -1439,7 +1525,7 @@ async function finishBlackjack(
.setFooter({
text:
`crumbBlackjackResult:${game.userId}:${game.bet}`
`crumbBlackjackResult:${game.userId}:${game.originalBet ?? game.bet}`
});
+61
View File
@@ -11,6 +11,10 @@ const {
temporaryEphemeralReply
} = require("./cookieEconomy");
const {
trackProfileEvent
} = require("./profileSystem");
/*
* ============================================================
@@ -852,6 +856,63 @@ async function playRoulette(
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 =
new EmbedBuilder()
.setTitle(
+78 -1415
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"
};
let profileSystemInitPromise = null;
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_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_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_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 = [
@@ -63,22 +73,34 @@ function getProfileCollections() {
}
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 });
if (profileSystemInitPromise) return profileSystemInitPromise;
for (const achievement of DEFAULT_ACHIEVEMENTS) {
await achievementConfig.updateOne(
{ achievementId: achievement.achievementId },
{ $setOnInsert: achievement },
{ upsert: true }
);
profileSystemInitPromise = (async () => {
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 });
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) {
await ensureProfileSystem();
const { badges } = getProfileCollections();
for (const badge of DEFAULT_BADGES) {
await badges.updateOne(
@@ -145,6 +167,7 @@ async function getProfileStats(userId, guildId) {
}
async function trackProfileEvent(interaction, userId, event) {
await ensureProfileSystem();
const guildId = interaction.guildId;
if (!guildId || !userId || !event?.type) return;
@@ -190,7 +213,7 @@ async function trackProfileEvent(interaction, userId, event) {
}
const updatedStats = await getProfileStats(userId, guildId);
await evaluateAchievements(interaction, userId, updatedStats);
await evaluateAchievements(interaction, userId, updatedStats, event.type);
await evaluateBadges(interaction, userId, 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 guildId = interaction.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 unlocked = unlockedDoc?.unlocked ?? {};
@@ -265,7 +291,7 @@ async function evaluateAchievements(interaction, userId, statsDocument) {
);
if (result.modifiedCount === 1) {
await sendAchievementNotification(interaction, definition);
await sendAchievementNotification(interaction, userId, definition);
}
}
}
@@ -314,23 +340,38 @@ async function tryClaimBadge(interaction, userId, badgeId) {
} catch {}
}
async function sendAchievementNotification(interaction, achievement) {
async function sendAchievementNotification(interaction, userId, achievement) {
try {
await interaction.followUp({
await interaction.channel.send({
embeds: [
new EmbedBuilder()
.setTitle("🏆 Achievement Unlocked!")
.setDescription([
`<@${userId}>`,
"",
`${achievement.emoji} **${achievement.name}**`,
"",
achievement.description
].join("\n"))
],
flags: MessageFlags.Ephemeral
]
});
} 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) {
const value = definition.stat
.split(".")
@@ -344,6 +385,7 @@ function readStatValue(document, definition) {
}
async function buildProfileOverview(guildId, user) {
await ensureProfileSystem();
const stats = await getProfileStats(user.id, guildId);
const balance = await getCookieBalance(user.id);
const { achievements, badges, achievementConfig } = getProfileCollections();
@@ -386,6 +428,7 @@ async function buildProfileOverview(guildId, user) {
}
async function buildBadgesEmbed(guildId, user) {
await ensureProfileSystem();
await ensureGuildBadges(guildId);
const { badges } = getProfileCollections();
const documents = await badges.find({ guildId }).toArray();
@@ -416,6 +459,7 @@ async function buildBadgesEmbed(guildId, user) {
}
async function buildAchievementsEmbed(guildId, user) {
await ensureProfileSystem();
const { achievements, achievementConfig } = getProfileCollections();
const [unlockedDoc, definitions] = await Promise.all([
@@ -424,9 +468,16 @@ async function buildAchievementsEmbed(guildId, user) {
]);
const unlocked = unlockedDoc?.unlocked ?? {};
const lines = definitions.map(a =>
`${unlocked[a.achievementId] ? "✅" : "⬜"} ${a.emoji} **${a.name}** — ${a.requirement}`
);
const stats = await getProfileStats(user.id, guildId);
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()
.setTitle(`🏆 ${user.globalName ?? user.username}'s Achievements`)
@@ -434,10 +485,67 @@ async function buildAchievementsEmbed(guildId, user) {
.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() {
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("profileBadges").setLabel("Badges").setEmoji("🏅").setStyle(ButtonStyle.Secondary),
new ButtonBuilder().setCustomId("profileAchievements").setLabel("Achievements").setEmoji("🏆").setStyle(ButtonStyle.Secondary),
new ButtonBuilder().setCustomId("abort").setLabel("Close").setEmoji("✖️").setStyle(ButtonStyle.Secondary)
@@ -457,6 +565,7 @@ module.exports = {
trackProfileEvent,
getProfileStats,
buildProfileOverview,
buildStatsEmbed,
buildBadgesEmbed,
buildAchievementsEmbed,
buildProfileButtons,