updated profile and centralized functions

This commit is contained in:
DeSqBlocki
2026-08-22 23:33:20 +02:00
parent 08d1a8c5cd
commit 030b4b8883
18 changed files with 3747 additions and 1202 deletions
+4 -26
View File
@@ -15,7 +15,8 @@ const {
} = require("./cookieEconomy");
const {
trackProfileEvent
trackProfileEvent,
recordGameOutcome
} = require("./profileSystem");
const HEIST_COOLDOWN =
@@ -255,12 +256,10 @@ async function resolveCookieHeist(
throw error;
}
await trackProfileEvent(
await recordGameOutcome(
interaction,
playerId,
{
type:
"game_result",
game:
"heist",
result:
@@ -280,15 +279,6 @@ async function resolveCookieHeist(
}
);
await trackProfileEvent(
interaction,
playerId,
{
type:
"cookie_gain",
amount
}
);
const [
playerAfter,
@@ -444,12 +434,10 @@ async function resolveCookieHeist(
throw error;
}
await trackProfileEvent(
await recordGameOutcome(
interaction,
playerId,
{
type:
"game_result",
game:
"heist",
result:
@@ -459,16 +447,6 @@ async function resolveCookieHeist(
}
);
await trackProfileEvent(
interaction,
playerId,
{
type:
"cookie_loss",
amount:
penalty
}
);
const [
playerAfter,
+2 -30
View File
@@ -18,7 +18,7 @@ const {
} = require("./cookieEconomy");
const {
trackProfileEvent
recordGameOutcome
} = require("./profileSystem");
const GAME_COLLECTION =
@@ -929,12 +929,10 @@ async function finishBlackjack(
profileResult = "loss";
}
await trackProfileEvent(
await recordGameOutcome(
interaction,
game.userId,
{
type:
"game_result",
game:
"blackjack",
result:
@@ -949,32 +947,6 @@ async function finishBlackjack(
}
);
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(
+2 -30
View File
@@ -12,7 +12,7 @@ const {
} = require("./cookieEconomy");
const {
trackProfileEvent
recordGameOutcome
} = require("./profileSystem");
const ROULETTE_BETS = [
@@ -577,12 +577,10 @@ async function playRoulette(
const profit =
payout - bet;
await trackProfileEvent(
await recordGameOutcome(
interaction,
playerId,
{
type:
"game_result",
game:
"roulette",
result:
@@ -602,32 +600,6 @@ async function playRoulette(
}
);
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(
+2 -30
View File
@@ -13,7 +13,7 @@ const {
} = require("./cookieEconomy");
const {
trackProfileEvent
recordGameOutcome
} = require("./profileSystem");
const SLOT_BETS = [
@@ -308,12 +308,10 @@ async function playSlots(
const profit =
payout - bet;
await trackProfileEvent(
await recordGameOutcome(
interaction,
playerId,
{
type:
"game_result",
game:
"slots",
result:
@@ -331,32 +329,6 @@ async function playSlots(
}
);
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(
+216
View File
@@ -0,0 +1,216 @@
const {
PermissionFlagsBits,
MessageFlags
} = require(
"discord.js"
);
/*
* ============================================================
* CONFIG
* ============================================================
*
* .env:
*
* BOT_AUTHOR_ID=123456789012345678
*
* Do not hard-code the author ID into commands.
*/
function getBotAuthorId() {
return (
process.env.BOT_AUTHOR_ID
??
null
);
}
/*
* ============================================================
* BOT AUTHOR
* ============================================================
*/
function isBotAuthor(
userId
) {
const authorId =
getBotAuthorId();
if (
!authorId
||
!userId
) {
return false;
}
return (
String(
userId
)
===
String(
authorId
)
);
}
/*
* ============================================================
* GUILD ADMINISTRATOR
* ============================================================
*/
function isAdministrator(
interaction
) {
if (
!interaction
?.memberPermissions
) {
return false;
}
return interaction
.memberPermissions
.has(
PermissionFlagsBits.Administrator
);
}
/*
* ============================================================
* ADMIN COMMAND ACCESS
* ============================================================
*
* Access is granted when:
*
* - the user is the configured bot author
* OR
* - the user has Discord Administrator
*
* Bot author is checked first so the owner can use admin
* utilities even without Discord Administrator in the guild.
*/
function canUseAdminCommands(
interaction
) {
if (
!interaction
?.user
?.id
) {
return false;
}
return (
isBotAuthor(
interaction.user.id
)
||
isAdministrator(
interaction
)
);
}
/*
* ============================================================
* STANDARD ACCESS DENIAL
* ============================================================
*
* Administrative access failures are intentionally ephemeral.
*/
async function denyAdminAccess(
interaction,
message =
"You don't have permission to use this command."
) {
const payload = {
content:
message,
flags:
MessageFlags.Ephemeral
};
if (
interaction.replied
||
interaction.deferred
) {
return interaction.followUp(
payload
);
}
return interaction.reply(
payload
);
}
/*
* ============================================================
* REQUIRE ADMIN ACCESS
* ============================================================
*
* Convenience helper:
*
* if (!await requireAdminAccess(interaction)) {
* return;
* }
*/
async function requireAdminAccess(
interaction
) {
if (
canUseAdminCommands(
interaction
)
) {
return true;
}
await denyAdminAccess(
interaction
);
return false;
}
/*
* ============================================================
* EXPORTS
* ============================================================
*/
module.exports = {
getBotAuthorId,
isBotAuthor,
isAdministrator,
canUseAdminCommands,
requireAdminAccess,
denyAdminAccess
};
+403
View File
@@ -0,0 +1,403 @@
const {
getProfileCollections,
getProfileStats,
readStatValue
} = require("./profileSystem");
const {
getUnixTime
} = require("./cookieEconomy");
function getActiveDefinitions(
achievementConfig
) {
return achievementConfig
.find({
active: {
$ne: false
}
})
.toArray();
}
async function reconcileUserAchievements({
guildId,
userId,
dryRun = false
}) {
const {
achievements,
achievementConfig
} = getProfileCollections();
const [
statsDocument,
definitions,
achievementDocument
] = await Promise.all([
getProfileStats(
userId,
guildId
),
getActiveDefinitions(
achievementConfig
),
achievements.findOne({
userId,
guildId
})
]);
const unlocked = {
...(achievementDocument?.unlocked ?? {})
};
const additions = {};
let skippedEventOnly = 0;
const normalDefinitions =
definitions.filter(
definition =>
definition.measure !==
"achievementPercentage"
);
for (const definition of normalDefinitions) {
if (unlocked[definition.achievementId]) {
continue;
}
if (!definition.stat) {
skippedEventOnly++;
continue;
}
const currentValue =
readStatValue(
statsDocument,
definition
);
if (
currentValue <
Number(definition.target ?? 0)
) {
continue;
}
const timestamp =
getUnixTime();
additions[
definition.achievementId
] = timestamp;
unlocked[
definition.achievementId
] = timestamp;
}
const collectibleDefinitions =
definitions.filter(
definition =>
definition.measure !==
"achievementPercentage"
);
const unlockedCollectible =
collectibleDefinitions.filter(
definition =>
Boolean(
unlocked[
definition.achievementId
]
)
).length;
const percentage =
collectibleDefinitions.length > 0
? unlockedCollectible /
collectibleDefinitions.length
: 0;
for (
const definition
of definitions.filter(
item =>
item.measure ===
"achievementPercentage"
)
) {
if (
unlocked[
definition.achievementId
]
) {
continue;
}
if (
percentage <
Number(definition.target ?? 0)
) {
continue;
}
additions[
definition.achievementId
] = getUnixTime();
}
const additionEntries =
Object.entries(additions);
if (
!dryRun &&
additionEntries.length > 0
) {
const set = {};
for (
const [achievementId, timestamp]
of additionEntries
) {
set[
`unlocked.${achievementId}`
] = timestamp;
}
await achievements.updateOne(
{
userId,
guildId
},
{
$setOnInsert: {
userId,
guildId
},
$set: set
},
{
upsert: true
}
);
}
return {
userId,
checked:
definitions.length,
unlocked:
additionEntries.length,
skippedEventOnly,
dryRun,
achievementIds:
additionEntries.map(
([achievementId]) =>
achievementId
)
};
}
async function reconcileAllAchievements({
guildId,
dryRun = false
}) {
const {
stats
} = getProfileCollections();
const userIds =
await stats.distinct(
"userId",
{
guildId
}
);
const summary = {
users:
userIds.length,
unlocked:
0,
skippedEventOnly:
0,
results: []
};
for (const userId of userIds) {
const result =
await reconcileUserAchievements({
guildId,
userId,
dryRun
});
summary.unlocked +=
result.unlocked;
summary.skippedEventOnly +=
result.skippedEventOnly;
summary.results.push(
result
);
}
return summary;
}
async function auditProfileData(
guildId
) {
const {
stats,
achievements,
achievementConfig,
badges
} = getProfileCollections();
const [
statDocuments,
achievementDocuments,
activeAchievements,
badgeDocuments
] = await Promise.all([
stats.find({
guildId
}).toArray(),
achievements.find({
guildId
}).toArray(),
achievementConfig.countDocuments({
active: {
$ne: false
}
}),
badges.find({
guildId
}).toArray()
]);
const issues = [];
for (const document of statDocuments) {
if (!document.userId) {
issues.push(
"Stats document missing userId."
);
continue;
}
const total =
document.games?.total ?? {};
const played =
Number(total.played ?? 0);
const resolved =
Number(total.won ?? 0) +
Number(total.lost ?? 0) +
Number(total.pushed ?? 0);
if (played !== resolved) {
issues.push(
`${document.userId}: games.total.played (${played}) does not match W/L/P total (${resolved}).`
);
}
for (
const game
of [
"slots",
"blackjack",
"roulette",
"heist"
]
) {
const value =
document.games?.[game] ?? {};
const gamePlayed =
Number(value.played ?? 0);
const gameResolved =
Number(value.won ?? 0) +
Number(value.lost ?? 0) +
Number(value.pushed ?? 0);
if (
gamePlayed !==
gameResolved
) {
issues.push(
`${document.userId}: games.${game}.played (${gamePlayed}) does not match W/L/P total (${gameResolved}).`
);
}
}
const nonNegativeFields = [
document.economy?.cookiesObtainedTotal,
document.economy?.cookiesLostTotal,
document.economy?.cookiesGivenTotal,
document.economy?.stolenTotal,
document.economy?.stolenFromMeTotal,
document.economy?.vaultStolenTotal
];
if (
nonNegativeFields.some(
value =>
Number(value ?? 0) < 0
)
) {
issues.push(
`${document.userId}: one or more economy counters are negative.`
);
}
}
const statUsers =
new Set(
statDocuments.map(
document =>
document.userId
)
);
const orphanAchievementDocs =
achievementDocuments.filter(
document =>
!statUsers.has(
document.userId
)
).length;
const claimedBadges =
badgeDocuments.filter(
badge =>
Boolean(badge.ownerId)
).length;
return {
statDocuments:
statDocuments.length,
achievementDocuments:
achievementDocuments.length,
activeAchievements,
badgeDocuments:
badgeDocuments.length,
claimedBadges,
orphanAchievementDocs,
issueCount:
issues.length,
issues
};
}
module.exports = {
reconcileUserAchievements,
reconcileAllAchievements,
auditProfileData
};
+225
View File
@@ -0,0 +1,225 @@
const {
EmbedBuilder
} = require("discord.js");
const {
getProfileCollections
} = require("./profileSystem");
const {
getCollections
} = require("./cookieEconomy");
const RECORDS = {
balance: {
label:
"🍪 Current Balance",
source:
"cookies",
field:
"cookies",
format:
value =>
`${Number(value ?? 0).toLocaleString()} 🍪`
},
obtained: {
label:
"🍪 Lifetime Cookies Obtained",
field:
"economy.cookiesObtainedTotal",
format:
value =>
`${Number(value ?? 0).toLocaleString()} 🍪`
},
given: {
label:
"🎁 Cookies Given",
field:
"economy.cookiesGivenTotal",
format:
value =>
`${Number(value ?? 0).toLocaleString()} 🍪`
},
stolen: {
label:
"🥷 Cookies Stolen",
field:
"economy.stolenTotal",
format:
value =>
`${Number(value ?? 0).toLocaleString()} 🍪`
},
vault: {
label:
"🏦 Bot Vault Stolen",
field:
"economy.vaultStolenTotal",
format:
value =>
`${Number(value ?? 0).toLocaleString()} 🍪`
},
wins: {
label:
"🏆 Games Won",
field:
"games.total.won",
format:
value =>
Number(value ?? 0).toLocaleString()
},
played: {
label:
"🎮 Games Played",
field:
"games.total.played",
format:
value =>
Number(value ?? 0).toLocaleString()
},
streak: {
label:
"🔥 Best Win Streak",
field:
"streaks.bestWin",
format:
value =>
Number(value ?? 0).toLocaleString()
},
lost: {
label:
"💸 Lifetime Cookies Lost",
field:
"economy.cookiesLostTotal",
format:
value =>
`${Number(value ?? 0).toLocaleString()} 🍪`
}
};
function getNestedValue(
document,
path
) {
return path
.split(".")
.reduce(
(value, key) =>
value?.[key],
document
);
}
async function buildRecordsEmbed({
guildId,
category,
botUserId,
limit = 10
}) {
const definition =
RECORDS[category] ??
RECORDS.balance;
let rows;
if (
definition.source ===
"cookies"
) {
const {
cookies
} = getCollections();
rows =
await cookies
.find({
userId: {
$ne: botUserId
},
cookies: {
$gt: 0
}
})
.sort({
cookies: -1
})
.limit(limit)
.toArray();
}
else {
const {
stats
} = getProfileCollections();
rows =
await stats
.find({
guildId,
userId: {
$ne: botUserId
},
[definition.field]: {
$gt: 0
}
})
.sort({
[definition.field]: -1
})
.limit(limit)
.toArray();
}
const medals = [
"🥇",
"🥈",
"🥉"
];
const description =
rows.length > 0
? rows.map(
(row, index) => {
const value =
getNestedValue(
row,
definition.field
);
const rank =
medals[index] ??
`**${index + 1}.**`;
return `${rank} <@${row.userId}> — **${definition.format(value)}**`;
}
).join("\n")
: "No qualifying records yet.";
return new EmbedBuilder()
.setTitle(
`📚 Records — ${definition.label}`
)
.setDescription(
description
)
.setFooter({
text:
"Records are derived from persisted profile statistics."
})
.setTimestamp();
}
function getRecordChoices() {
return Object.entries(RECORDS)
.map(
([value, definition]) => ({
name:
definition.label,
value
})
);
}
module.exports = {
buildRecordsEmbed,
getRecordChoices
};
+2083 -756
View File
File diff suppressed because it is too large Load Diff