Compare commits
2 Commits
81ef97370c
...
9847e476b8
| Author | SHA1 | Date | |
|---|---|---|---|
| 9847e476b8 | |||
| 8aeab3fb5d |
@@ -1,7 +1,7 @@
|
|||||||
const {
|
const {
|
||||||
showSlots
|
showSlots
|
||||||
} = require(
|
} = require(
|
||||||
"../../utils/crumbSaucer"
|
"../../utils/crumbSlots.js"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const {
|
const {
|
||||||
playSlots
|
playSlots
|
||||||
} = require(
|
} = require(
|
||||||
"../../utils/crumbSaucer"
|
"../../utils/crumbSlots.js"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const {
|
const {
|
||||||
playSlots
|
playSlots
|
||||||
} = require(
|
} = require(
|
||||||
"../../utils/crumbSaucer"
|
"../../utils/crumbSlots.js"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const {
|
const {
|
||||||
playSlots
|
playSlots
|
||||||
} = require(
|
} = require(
|
||||||
"../../utils/crumbSaucer"
|
"../../utils/crumbSlots.js"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const {
|
const {
|
||||||
playSlots
|
playSlots
|
||||||
} = require(
|
} = require(
|
||||||
"../../utils/crumbSaucer"
|
"../../utils/crumbSlots.js"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const {
|
const {
|
||||||
playSlots
|
playSlots
|
||||||
} = require(
|
} = require(
|
||||||
"../../utils/crumbSaucer"
|
"../../utils/crumbSlots.js"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
const {
|
||||||
|
buildAchievementsEmbed,
|
||||||
|
buildProfileButtons,
|
||||||
|
getProfileTargetId
|
||||||
|
} = require("../../utils/profileSystem");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: "profileAchievements",
|
||||||
|
|
||||||
|
async execute(interaction) {
|
||||||
|
const userId = getProfileTargetId(interaction);
|
||||||
|
if (!userId) return;
|
||||||
|
|
||||||
|
const user = await interaction.client.users.fetch(userId);
|
||||||
|
const embed = await buildAchievementsEmbed(interaction.guildId, user);
|
||||||
|
|
||||||
|
return interaction.update({
|
||||||
|
embeds: [embed],
|
||||||
|
components: buildProfileButtons()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
const {
|
||||||
|
buildBadgesEmbed,
|
||||||
|
buildProfileButtons,
|
||||||
|
getProfileTargetId
|
||||||
|
} = require("../../utils/profileSystem");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: "profileBadges",
|
||||||
|
|
||||||
|
async execute(interaction) {
|
||||||
|
const userId = getProfileTargetId(interaction);
|
||||||
|
if (!userId) return;
|
||||||
|
|
||||||
|
const user = await interaction.client.users.fetch(userId);
|
||||||
|
const embed = await buildBadgesEmbed(interaction.guildId, user);
|
||||||
|
|
||||||
|
return interaction.update({
|
||||||
|
embeds: [embed],
|
||||||
|
components: buildProfileButtons()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
const {
|
||||||
|
buildProfileOverview,
|
||||||
|
buildProfileButtons,
|
||||||
|
getProfileTargetId
|
||||||
|
} = require("../../utils/profileSystem");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: "profileOverview",
|
||||||
|
|
||||||
|
async execute(interaction) {
|
||||||
|
const userId = getProfileTargetId(interaction);
|
||||||
|
if (!userId) return;
|
||||||
|
|
||||||
|
const user = await interaction.client.users.fetch(userId);
|
||||||
|
const embed = await buildProfileOverview(interaction.guildId, user);
|
||||||
|
|
||||||
|
return interaction.update({
|
||||||
|
embeds: [embed],
|
||||||
|
components: buildProfileButtons()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
const { SlashCommandBuilder } = require("discord.js");
|
||||||
|
|
||||||
|
const {
|
||||||
|
buildProfileOverview,
|
||||||
|
buildProfileButtons,
|
||||||
|
ensureGuildBadges
|
||||||
|
} = require("../../utils/profileSystem");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("profile")
|
||||||
|
.setDescription("View a player profile")
|
||||||
|
.addUserOption(option =>
|
||||||
|
option
|
||||||
|
.setName("user")
|
||||||
|
.setDescription("Player to view")
|
||||||
|
),
|
||||||
|
|
||||||
|
async execute(interaction) {
|
||||||
|
const user = interaction.options.getUser("user") ?? interaction.user;
|
||||||
|
|
||||||
|
await ensureGuildBadges(interaction.guildId);
|
||||||
|
|
||||||
|
const embed = await buildProfileOverview(
|
||||||
|
interaction.guildId,
|
||||||
|
user
|
||||||
|
);
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [embed],
|
||||||
|
components: buildProfileButtons()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
const {
|
||||||
|
EmbedBuilder,
|
||||||
|
ActionRowBuilder,
|
||||||
|
ButtonBuilder,
|
||||||
|
ButtonStyle
|
||||||
|
} = require("discord.js");
|
||||||
|
|
||||||
|
const {
|
||||||
|
getCookieBalance,
|
||||||
|
transferCookies,
|
||||||
|
temporaryEphemeralReply,
|
||||||
|
randomItem
|
||||||
|
} = require("./cookieEconomy");
|
||||||
|
|
||||||
|
const {
|
||||||
|
trackProfileEvent
|
||||||
|
} = require("./profileSystem");
|
||||||
|
|
||||||
|
const SLOT_BETS = [10, 50, 100, 500, 1000];
|
||||||
|
|
||||||
|
const SLOT_SYMBOLS = [
|
||||||
|
"🍒",
|
||||||
|
"🍋",
|
||||||
|
"🍪",
|
||||||
|
"⭐",
|
||||||
|
"💎"
|
||||||
|
];
|
||||||
|
|
||||||
|
const SLOT_OUTCOMES = [
|
||||||
|
{ weight: 300, type: "loss", multiplier: 0 },
|
||||||
|
{ weight: 390, type: "pair", multiplier: 0.5 },
|
||||||
|
{ weight: 150, type: "triple", symbol: "🍋", multiplier: 1.5 },
|
||||||
|
{ weight: 80, type: "triple", symbol: "🍒", multiplier: 2 },
|
||||||
|
{ weight: 45, type: "triple", symbol: "🍪", multiplier: 3 },
|
||||||
|
{ weight: 25, type: "triple", symbol: "⭐", multiplier: 5 },
|
||||||
|
{ weight: 10, type: "triple", symbol: "💎", multiplier: 10 }
|
||||||
|
];
|
||||||
|
|
||||||
|
async function showSlots(interaction) {
|
||||||
|
const ownerId = getSlotsSessionOwner(interaction);
|
||||||
|
|
||||||
|
if (ownerId !== interaction.user.id) {
|
||||||
|
return temporaryEphemeralReply(interaction, {
|
||||||
|
content: "This isn't your Crumb Saucer session."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const balance = await getCookieBalance(interaction.user.id);
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle("🎰 Crumb Saucer Slots")
|
||||||
|
.setDescription([
|
||||||
|
"Welcome to the Cookie reels.",
|
||||||
|
"",
|
||||||
|
"Choose your wager below.",
|
||||||
|
"",
|
||||||
|
"🍋 **Triple Lemon** — x1.5",
|
||||||
|
"🍒 **Triple Cherry** — x2",
|
||||||
|
"🍪 **Triple Cookie** — x3",
|
||||||
|
"⭐ **Triple Star** — x5",
|
||||||
|
"💎 **Jackpot** — x10"
|
||||||
|
].join("\n"))
|
||||||
|
.addFields({
|
||||||
|
name: "Your Vault",
|
||||||
|
value: `**${balance.toLocaleString()} 🍪**`
|
||||||
|
})
|
||||||
|
.setFooter({
|
||||||
|
text: `crumbSaucerSlots:${interaction.user.id}`
|
||||||
|
});
|
||||||
|
|
||||||
|
return interaction.update({
|
||||||
|
embeds: [embed],
|
||||||
|
components: buildSlotBetButtons()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function playSlots(interaction, bet) {
|
||||||
|
const ownerId = getSlotsSessionOwner(interaction);
|
||||||
|
|
||||||
|
if (ownerId !== interaction.user.id) {
|
||||||
|
return temporaryEphemeralReply(interaction, {
|
||||||
|
content: "These aren't your slots. Open your own Crumb Saucer session."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SLOT_BETS.includes(bet)) {
|
||||||
|
return temporaryEphemeralReply(interaction, {
|
||||||
|
content: "That wager isn't available."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerId = interaction.user.id;
|
||||||
|
const vaultId = interaction.client.user.id;
|
||||||
|
|
||||||
|
const [playerBalance, vaultBalance] = await Promise.all([
|
||||||
|
getCookieBalance(playerId),
|
||||||
|
getCookieBalance(vaultId)
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (playerBalance < bet) {
|
||||||
|
return temporaryEphemeralReply(interaction, {
|
||||||
|
content: `You need **${bet.toLocaleString()} Cookies** for that wager.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxPayout = bet * 10;
|
||||||
|
|
||||||
|
if (vaultBalance + bet < maxPayout) {
|
||||||
|
return temporaryEphemeralReply(interaction, {
|
||||||
|
content: "The Crumb Saucer vault doesn't currently have enough Cookies to cover that wager."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await transferCookies(playerId, vaultId, bet);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.message === "INSUFFICIENT_COOKIES") {
|
||||||
|
return temporaryEphemeralReply(interaction, {
|
||||||
|
content: "Your Cookie balance changed before the spin could begin."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.update({
|
||||||
|
embeds: [
|
||||||
|
buildSpinningEmbed(
|
||||||
|
bet,
|
||||||
|
["❔", "❔", "❔"],
|
||||||
|
"The reels are spinning..."
|
||||||
|
)
|
||||||
|
],
|
||||||
|
components: []
|
||||||
|
});
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const firstPreview = randomItem(SLOT_SYMBOLS);
|
||||||
|
|
||||||
|
await interaction.editReply({
|
||||||
|
embeds: [
|
||||||
|
buildSpinningEmbed(
|
||||||
|
bet,
|
||||||
|
[firstPreview, "❔", "❔"],
|
||||||
|
"The first reel stops..."
|
||||||
|
)
|
||||||
|
],
|
||||||
|
components: []
|
||||||
|
});
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const secondPreview = randomItem(SLOT_SYMBOLS);
|
||||||
|
|
||||||
|
await interaction.editReply({
|
||||||
|
embeds: [
|
||||||
|
buildSpinningEmbed(
|
||||||
|
bet,
|
||||||
|
[firstPreview, secondPreview, "❔"],
|
||||||
|
"One more..."
|
||||||
|
)
|
||||||
|
],
|
||||||
|
components: []
|
||||||
|
});
|
||||||
|
|
||||||
|
await sleep(600);
|
||||||
|
|
||||||
|
const outcome = rollSlotOutcome();
|
||||||
|
const result = generateOutcomeLine(outcome);
|
||||||
|
const payout = Math.floor(bet * outcome.multiplier);
|
||||||
|
|
||||||
|
if (payout > 0) {
|
||||||
|
try {
|
||||||
|
await transferCookies(vaultId, playerId, payout);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("CRITICAL: Crumb Saucer Slots payout failed:", error);
|
||||||
|
|
||||||
|
return interaction.editReply({
|
||||||
|
embeds: [
|
||||||
|
new EmbedBuilder()
|
||||||
|
.setTitle("🎰 Crumb Saucer Error")
|
||||||
|
.setDescription(
|
||||||
|
"The reels stopped, but the payout system had a problem. Please contact an administrator."
|
||||||
|
)
|
||||||
|
.setFooter({
|
||||||
|
text: `crumbSaucerSlotsResult:${playerId}:${bet}`
|
||||||
|
})
|
||||||
|
],
|
||||||
|
components: buildResultButtons()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [playerAfter, vaultAfter] = await Promise.all([
|
||||||
|
getCookieBalance(playerId),
|
||||||
|
getCookieBalance(vaultId)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const profit = payout - bet;
|
||||||
|
|
||||||
|
await trackProfileEvent(interaction, playerId, {
|
||||||
|
type: "game_result",
|
||||||
|
game: "slots",
|
||||||
|
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("🎰 Crumb Saucer Slots")
|
||||||
|
.setDescription([
|
||||||
|
"## " + result.join(" "),
|
||||||
|
"",
|
||||||
|
`### ${getSlotResultName(outcome)}`
|
||||||
|
].join("\n"))
|
||||||
|
.addFields(
|
||||||
|
{ name: "Bet", value: `${bet.toLocaleString()} 🍪`, inline: true },
|
||||||
|
{ name: "Multiplier", value: `x${outcome.multiplier}`, inline: true },
|
||||||
|
{ name: "Payout", value: `${payout.toLocaleString()} 🍪`, inline: true },
|
||||||
|
{ name: "Profit", value: `${profit >= 0 ? "+" : ""}${profit.toLocaleString()} 🍪`, inline: true },
|
||||||
|
{ name: "Your Vault", value: `${playerAfter.toLocaleString()} 🍪`, inline: true },
|
||||||
|
{ name: "House Vault", value: `${vaultAfter.toLocaleString()} 🍪`, inline: true }
|
||||||
|
)
|
||||||
|
.setFooter({
|
||||||
|
text: `crumbSaucerSlotsResult:${playerId}:${bet}`
|
||||||
|
});
|
||||||
|
|
||||||
|
return interaction.editReply({
|
||||||
|
embeds: [resultEmbed],
|
||||||
|
components: buildResultButtons()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runSlotsBack(interaction) {
|
||||||
|
const footer = interaction.message?.embeds?.[0]?.footer?.text;
|
||||||
|
const parts = footer?.split(":") ?? [];
|
||||||
|
|
||||||
|
if (parts[0] !== "crumbSaucerSlotsResult") {
|
||||||
|
return temporaryEphemeralReply(interaction, {
|
||||||
|
content: "This Slots result can no longer be replayed."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ownerId = parts[1];
|
||||||
|
const bet = Number(parts[2]);
|
||||||
|
|
||||||
|
if (ownerId !== interaction.user.id) {
|
||||||
|
return temporaryEphemeralReply(interaction, {
|
||||||
|
content: "These aren't your slots."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SLOT_BETS.includes(bet)) {
|
||||||
|
return temporaryEphemeralReply(interaction, {
|
||||||
|
content: "The previous wager is no longer available."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return playSlots(interaction, bet);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSlotBetButtons() {
|
||||||
|
return [
|
||||||
|
new ActionRowBuilder().addComponents(
|
||||||
|
new ButtonBuilder().setCustomId("crumbSaucerSlots10").setLabel("10").setEmoji("🍪").setStyle(ButtonStyle.Secondary),
|
||||||
|
new ButtonBuilder().setCustomId("crumbSaucerSlots50").setLabel("50").setEmoji("🍪").setStyle(ButtonStyle.Secondary),
|
||||||
|
new ButtonBuilder().setCustomId("crumbSaucerSlots100").setLabel("100").setEmoji("🍪").setStyle(ButtonStyle.Primary),
|
||||||
|
new ButtonBuilder().setCustomId("crumbSaucerSlots500").setLabel("500").setEmoji("🍪").setStyle(ButtonStyle.Primary),
|
||||||
|
new ButtonBuilder().setCustomId("crumbSaucerSlots1000").setLabel("1,000").setEmoji("🍪").setStyle(ButtonStyle.Danger)
|
||||||
|
),
|
||||||
|
new ActionRowBuilder().addComponents(
|
||||||
|
new ButtonBuilder().setCustomId("crumbSaucerBack").setLabel("Back to Crumb Saucer").setEmoji("◀️").setStyle(ButtonStyle.Secondary)
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildResultButtons() {
|
||||||
|
return [
|
||||||
|
new ActionRowBuilder().addComponents(
|
||||||
|
new ButtonBuilder().setCustomId("crumbSaucerSlotsRunItBack").setLabel("Run It Back").setEmoji("🔁").setStyle(ButtonStyle.Success),
|
||||||
|
new ButtonBuilder().setCustomId("crumbSaucerSlots").setLabel("Change Bet").setEmoji("🎰").setStyle(ButtonStyle.Primary),
|
||||||
|
new ButtonBuilder().setCustomId("crumbSaucerBack").setLabel("Crumb Saucer").setEmoji("◀️").setStyle(ButtonStyle.Secondary)
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function rollSlotOutcome() {
|
||||||
|
const totalWeight = SLOT_OUTCOMES.reduce(
|
||||||
|
(total, outcome) => total + outcome.weight,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
let roll = Math.random() * totalWeight;
|
||||||
|
|
||||||
|
for (const outcome of SLOT_OUTCOMES) {
|
||||||
|
roll -= outcome.weight;
|
||||||
|
if (roll <= 0) return outcome;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SLOT_OUTCOMES[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateRandomLine() {
|
||||||
|
return [
|
||||||
|
randomItem(SLOT_SYMBOLS),
|
||||||
|
randomItem(SLOT_SYMBOLS),
|
||||||
|
randomItem(SLOT_SYMBOLS)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateOutcomeLine(outcome) {
|
||||||
|
if (outcome.type === "triple") {
|
||||||
|
return [
|
||||||
|
outcome.symbol,
|
||||||
|
outcome.symbol,
|
||||||
|
outcome.symbol
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outcome.type === "pair") {
|
||||||
|
const symbol = randomItem(SLOT_SYMBOLS);
|
||||||
|
let other;
|
||||||
|
|
||||||
|
do {
|
||||||
|
other = randomItem(SLOT_SYMBOLS);
|
||||||
|
} while (other === symbol);
|
||||||
|
|
||||||
|
const result = [symbol, symbol, symbol];
|
||||||
|
result[Math.floor(Math.random() * 3)] = other;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result;
|
||||||
|
|
||||||
|
do {
|
||||||
|
result = generateRandomLine();
|
||||||
|
} while (new Set(result).size < 3);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSpinningEmbed(bet, symbols, status) {
|
||||||
|
return new EmbedBuilder()
|
||||||
|
.setTitle("🎰 Crumb Saucer Slots")
|
||||||
|
.setDescription([
|
||||||
|
"## " + symbols.join(" "),
|
||||||
|
"",
|
||||||
|
`**${status}**`
|
||||||
|
].join("\n"))
|
||||||
|
.addFields({
|
||||||
|
name: "Bet",
|
||||||
|
value: `**${bet.toLocaleString()} 🍪**`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSlotResultName(outcome) {
|
||||||
|
if (outcome.type === "loss") return "No luck this time.";
|
||||||
|
if (outcome.type === "pair") return "Pair!";
|
||||||
|
|
||||||
|
switch (outcome.symbol) {
|
||||||
|
case "🍋": return "🍋 Triple Lemon!";
|
||||||
|
case "🍒": return "🍒 Triple Cherry!";
|
||||||
|
case "🍪": return "🍪 Triple Cookie!";
|
||||||
|
case "⭐": return "⭐ Triple Star!";
|
||||||
|
case "💎": return "💎 JACKPOT! 💎";
|
||||||
|
default: return "Winner!";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSlotsSessionOwner(interaction) {
|
||||||
|
const footer = interaction.message?.embeds?.[0]?.footer?.text;
|
||||||
|
const parts = footer?.split(":") ?? [];
|
||||||
|
|
||||||
|
if (parts[0] === "crumbSaucer") return parts[1];
|
||||||
|
if (parts[0] === "crumbSaucerSlots") return parts[1];
|
||||||
|
if (parts[0] === "crumbSaucerSlotsResult") return parts[1];
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
showSlots,
|
||||||
|
playSlots,
|
||||||
|
runSlotsBack
|
||||||
|
};
|
||||||
@@ -0,0 +1,464 @@
|
|||||||
|
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"
|
||||||
|
};
|
||||||
|
|
||||||
|
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: "🕵️" },
|
||||||
|
{ achievementId: "stolen_100000", name: "Grand Theft Cookie", description: "Steal 100,000 Cookies from other players.", requirement: "Steal 100,000 Cookies total.", category: "stealing", stat: "economy.stolenTotal", target: 100000, emoji: "💰" },
|
||||||
|
{ achievementId: "stolen_unique_3", name: "Making the Rounds", description: "Successfully steal from 3 different players.", requirement: "Steal from 3 unique players.", category: "stealing", stat: "unique.playersStolenFrom", measure: "arrayLength", target: 3, emoji: "🎯" },
|
||||||
|
{ achievementId: "stolen_unique_10", name: "Equal Opportunity Thief", description: "Successfully steal from 10 different players.", requirement: "Steal from 10 unique players.", category: "stealing", stat: "unique.playersStolenFrom", measure: "arrayLength", target: 10, emoji: "🗺️" },
|
||||||
|
{ achievementId: "stolen_from_me_1000", name: "Easy Pickings", description: "Have 1,000 Cookies stolen from you.", requirement: "Lose 1,000 Cookies to other players stealing.", category: "victim", stat: "economy.stolenFromMeTotal", target: 1000, emoji: "💸" },
|
||||||
|
{ achievementId: "stolen_from_me_10000", name: "Lock the Door", description: "Have 10,000 Cookies stolen from you.", requirement: "Lose 10,000 Cookies to other players stealing.", category: "victim", stat: "economy.stolenFromMeTotal", target: 10000, emoji: "🚪" },
|
||||||
|
{ achievementId: "stolen_by_unique_3", name: "Marked", description: "Be successfully stolen from by 3 different players.", requirement: "Be stolen from by 3 unique players.", category: "victim", stat: "unique.playersStolenBy", measure: "arrayLength", target: 3, emoji: "👀" },
|
||||||
|
{ achievementId: "stolen_by_unique_10", name: "Criminal Magnet", description: "Be successfully stolen from by 10 different players.", requirement: "Be stolen from by 10 unique players.", category: "victim", stat: "unique.playersStolenBy", measure: "arrayLength", target: 10, emoji: "🧲" },
|
||||||
|
{ achievementId: "obtained_10000", name: "Growing Stash", description: "Obtain 10,000 Cookies over your lifetime.", requirement: "Obtain 10,000 Cookies from all sources.", category: "economy", stat: "economy.cookiesObtainedTotal", target: 10000, emoji: "🍪" },
|
||||||
|
{ achievementId: "obtained_100000", name: "Cookie Magnate", description: "Obtain 100,000 Cookies over your lifetime.", requirement: "Obtain 100,000 Cookies from all sources.", category: "economy", stat: "economy.cookiesObtainedTotal", target: 100000, emoji: "💰" },
|
||||||
|
{ achievementId: "obtained_1000000", name: "Crumb Tycoon", description: "Obtain 1,000,000 Cookies over your lifetime.", requirement: "Obtain 1,000,000 Cookies from all sources.", category: "economy", stat: "economy.cookiesObtainedTotal", target: 1000000, emoji: "👑" },
|
||||||
|
{ achievementId: "vault_stolen_1000", name: "Testing the Locks", description: "Steal 1,000 Cookies from the Bot Vault.", requirement: "Steal 1,000 Cookies from the Bot Vault.", category: "vault", stat: "economy.vaultStolenTotal", target: 1000, emoji: "🏦" },
|
||||||
|
{ achievementId: "vault_stolen_10000", name: "Vault Regular", description: "Steal 10,000 Cookies from the Bot Vault.", requirement: "Steal 10,000 Cookies from the Bot Vault.", category: "vault", stat: "economy.vaultStolenTotal", target: 10000, emoji: "🥷" },
|
||||||
|
{ achievementId: "lost_10000", name: "Expensive Hobby", description: "Lose 10,000 Cookies.", requirement: "Lose 10,000 Cookies total.", category: "economy", stat: "economy.cookiesLostTotal", target: 10000, emoji: "🔥" },
|
||||||
|
{ achievementId: "lost_100000", name: "Financial Catastrophe", description: "Lose 100,000 Cookies.", requirement: "Lose 100,000 Cookies total.", category: "economy", stat: "economy.cookiesLostTotal", target: 100000, 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: "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: "🕳️" }
|
||||||
|
];
|
||||||
|
|
||||||
|
const DEFAULT_BADGES = [
|
||||||
|
{ badgeId: "first_100k_cookies", name: "Cookie Pioneer", description: "First player to hold 100,000 Cookies.", emoji: "🍪", ownerId: null, unlockedAt: null },
|
||||||
|
{ badgeId: "first_1m_cookies", name: "Cookie Millionaire", description: "First player to hold 1,000,000 Cookies.", emoji: "👑", ownerId: null, unlockedAt: null },
|
||||||
|
{ badgeId: "first_100k_vault_stolen", name: "Vault Breaker", description: "First player to steal 100,000 Cookies from the Bot Vault.", emoji: "🏦", ownerId: null, unlockedAt: null }
|
||||||
|
];
|
||||||
|
|
||||||
|
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 });
|
||||||
|
|
||||||
|
for (const achievement of DEFAULT_ACHIEVEMENTS) {
|
||||||
|
await achievementConfig.updateOne(
|
||||||
|
{ achievementId: achievement.achievementId },
|
||||||
|
{ $setOnInsert: achievement },
|
||||||
|
{ upsert: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureGuildBadges(guildId) {
|
||||||
|
const { badges } = getProfileCollections();
|
||||||
|
for (const badge of DEFAULT_BADGES) {
|
||||||
|
await badges.updateOne(
|
||||||
|
{ guildId, badgeId: badge.badgeId },
|
||||||
|
{ $setOnInsert: { guildId, ...badge } },
|
||||||
|
{ upsert: 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 },
|
||||||
|
blackjack: { played: 0, won: 0, lost: 0, pushed: 0 },
|
||||||
|
roulette: { played: 0, won: 0, lost: 0, pushed: 0 },
|
||||||
|
heist: { played: 0, won: 0, lost: 0, pushed: 0 }
|
||||||
|
},
|
||||||
|
streaks: {
|
||||||
|
currentWin: 0,
|
||||||
|
bestWin: 0,
|
||||||
|
currentLoss: 0,
|
||||||
|
bestLoss: 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;
|
||||||
|
|
||||||
|
const { stats } = getProfileCollections();
|
||||||
|
await ensureStatsDocument(userId, guildId);
|
||||||
|
|
||||||
|
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;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedStats = await getProfileStats(userId, guildId);
|
||||||
|
await evaluateAchievements(interaction, userId, updatedStats);
|
||||||
|
await evaluateBadges(interaction, userId, updatedStats);
|
||||||
|
return updatedStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 { achievements, achievementConfig } = getProfileCollections();
|
||||||
|
const guildId = interaction.guildId;
|
||||||
|
await ensureAchievementDocument(userId, guildId);
|
||||||
|
|
||||||
|
const definitions = await achievementConfig.find({}).toArray();
|
||||||
|
const unlockedDoc = await achievements.findOne({ userId, guildId });
|
||||||
|
const unlocked = unlockedDoc?.unlocked ?? {};
|
||||||
|
|
||||||
|
for (const definition of definitions) {
|
||||||
|
if (unlocked[definition.achievementId]) continue;
|
||||||
|
const currentValue = readStatValue(statsDocument, definition);
|
||||||
|
if (currentValue < definition.target) continue;
|
||||||
|
|
||||||
|
const field = `unlocked.${definition.achievementId}`;
|
||||||
|
const result = await achievements.updateOne(
|
||||||
|
{ userId, guildId, [field]: { $exists: false } },
|
||||||
|
{ $set: { [field]: getUnixTime() } }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.modifiedCount === 1) {
|
||||||
|
await sendAchievementNotification(interaction, definition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function evaluateBadges(interaction, userId, statsDocument) {
|
||||||
|
const guildId = interaction.guildId;
|
||||||
|
await ensureGuildBadges(guildId);
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryClaimBadge(interaction, userId, badgeId) {
|
||||||
|
const { badges } = getProfileCollections();
|
||||||
|
const guildId = interaction.guildId;
|
||||||
|
|
||||||
|
const badge = await badges.findOneAndUpdate(
|
||||||
|
{ guildId, badgeId, ownerId: null },
|
||||||
|
{ $set: { ownerId: userId, unlockedAt: getUnixTime() } },
|
||||||
|
{ returnDocument: "after" }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!badge || badge.ownerId !== userId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await interaction.channel.send({
|
||||||
|
embeds: [
|
||||||
|
new EmbedBuilder()
|
||||||
|
.setTitle("🏅 Unique Badge Claimed!")
|
||||||
|
.setDescription([
|
||||||
|
`${badge.emoji} **${badge.name}**`,
|
||||||
|
"",
|
||||||
|
`<@${userId}> claimed a guild-unique badge!`,
|
||||||
|
"",
|
||||||
|
badge.description,
|
||||||
|
"",
|
||||||
|
"*This badge can no longer be earned by another player.*"
|
||||||
|
].join("\n"))
|
||||||
|
]
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendAchievementNotification(interaction, achievement) {
|
||||||
|
try {
|
||||||
|
await interaction.followUp({
|
||||||
|
embeds: [
|
||||||
|
new EmbedBuilder()
|
||||||
|
.setTitle("🏆 Achievement Unlocked!")
|
||||||
|
.setDescription([
|
||||||
|
`${achievement.emoji} **${achievement.name}**`,
|
||||||
|
"",
|
||||||
|
achievement.description
|
||||||
|
].join("\n"))
|
||||||
|
],
|
||||||
|
flags: MessageFlags.Ephemeral
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readStatValue(document, definition) {
|
||||||
|
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 buildProfileOverview(guildId, user) {
|
||||||
|
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(),
|
||||||
|
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: `profile:${user.id}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildBadgesEmbed(guildId, user) {
|
||||||
|
await ensureGuildBadges(guildId);
|
||||||
|
const { badges } = getProfileCollections();
|
||||||
|
const documents = await badges.find({ guildId }).toArray();
|
||||||
|
|
||||||
|
const owned = documents.filter(badge => badge.ownerId === user.id);
|
||||||
|
const other = documents.filter(badge => badge.ownerId && badge.ownerId !== user.id);
|
||||||
|
const unclaimed = documents.filter(badge => !badge.ownerId);
|
||||||
|
|
||||||
|
return new EmbedBuilder()
|
||||||
|
.setTitle(`🏅 ${user.globalName ?? user.username}'s Badges`)
|
||||||
|
.setDescription([
|
||||||
|
"### Owned",
|
||||||
|
owned.length
|
||||||
|
? owned.map(b => `${b.emoji} **${b.name}**\n${b.description}`).join("\n\n")
|
||||||
|
: "*No unique badges yet.*",
|
||||||
|
"",
|
||||||
|
"### Claimed by Others",
|
||||||
|
other.length
|
||||||
|
? other.map(b => `${b.emoji} **${b.name}** — <@${b.ownerId}>`).join("\n")
|
||||||
|
: "*None yet.*",
|
||||||
|
"",
|
||||||
|
"### Still Available",
|
||||||
|
unclaimed.length
|
||||||
|
? unclaimed.map(b => `${b.emoji} **${b.name}**\n${b.description}`).join("\n\n")
|
||||||
|
: "*All unique badges have been claimed.*"
|
||||||
|
].join("\n"))
|
||||||
|
.setFooter({ text: `profile:${user.id}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildAchievementsEmbed(guildId, user) {
|
||||||
|
const { achievements, achievementConfig } = getProfileCollections();
|
||||||
|
|
||||||
|
const [unlockedDoc, definitions] = await Promise.all([
|
||||||
|
achievements.findOne({ userId: user.id, guildId }),
|
||||||
|
achievementConfig.find({}).sort({ category: 1, target: 1 }).toArray()
|
||||||
|
]);
|
||||||
|
|
||||||
|
const unlocked = unlockedDoc?.unlocked ?? {};
|
||||||
|
const lines = definitions.map(a =>
|
||||||
|
`${unlocked[a.achievementId] ? "✅" : "⬜"} ${a.emoji} **${a.name}** — ${a.requirement}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return new EmbedBuilder()
|
||||||
|
.setTitle(`🏆 ${user.globalName ?? user.username}'s Achievements`)
|
||||||
|
.setDescription(lines.join("\n") || "No achievements configured.")
|
||||||
|
.setFooter({ text: `profile:${user.id}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildProfileButtons() {
|
||||||
|
return [
|
||||||
|
new ActionRowBuilder().addComponents(
|
||||||
|
new ButtonBuilder().setCustomId("profileOverview").setLabel("Overview").setEmoji("👤").setStyle(ButtonStyle.Primary),
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProfileTargetId(interaction) {
|
||||||
|
const footer = interaction.message?.embeds?.[0]?.footer?.text;
|
||||||
|
const [type, userId] = footer?.split(":") ?? [];
|
||||||
|
return type === "profile" ? userId : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ensureProfileSystem,
|
||||||
|
ensureGuildBadges,
|
||||||
|
trackProfileEvent,
|
||||||
|
getProfileStats,
|
||||||
|
buildProfileOverview,
|
||||||
|
buildBadgesEmbed,
|
||||||
|
buildAchievementsEmbed,
|
||||||
|
buildProfileButtons,
|
||||||
|
getProfileTargetId
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user