snapshot rollout phase 1
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
node_modules
|
||||
.env
|
||||
backup/
|
||||
@@ -0,0 +1,58 @@
|
||||
const {
|
||||
buildProfileOverview,
|
||||
buildProfileButtons
|
||||
} = require("../../utils/profileSystem");
|
||||
|
||||
const {
|
||||
temporaryEphemeralReply
|
||||
} = require("../../utils/cookieEconomy");
|
||||
|
||||
module.exports = {
|
||||
name: "crumbSaucerProfile",
|
||||
|
||||
async execute(interaction) {
|
||||
const footer =
|
||||
interaction.message
|
||||
?.embeds?.[0]
|
||||
?.footer
|
||||
?.text;
|
||||
|
||||
const [
|
||||
type,
|
||||
ownerId
|
||||
] = footer?.split(":") ?? [];
|
||||
|
||||
|
||||
if (
|
||||
type !== "crumbSaucer" ||
|
||||
ownerId !== interaction.user.id
|
||||
) {
|
||||
return temporaryEphemeralReply(
|
||||
interaction,
|
||||
{
|
||||
content:
|
||||
"This isn't your Crumb Saucer session."
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const embed =
|
||||
await buildProfileOverview(
|
||||
interaction.guildId,
|
||||
interaction.user,
|
||||
interaction.user.id
|
||||
);
|
||||
|
||||
|
||||
return interaction.update({
|
||||
embeds: [embed],
|
||||
components:
|
||||
buildProfileButtons(
|
||||
"overview",
|
||||
0,
|
||||
1
|
||||
)
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
const {
|
||||
SlashCommandBuilder,
|
||||
PermissionFlagsBits,
|
||||
MessageFlags,
|
||||
EmbedBuilder
|
||||
} = require("discord.js");
|
||||
@@ -13,6 +12,13 @@ const {
|
||||
"../../utils/profileMaintenance"
|
||||
);
|
||||
|
||||
const {
|
||||
requireAdminAccess
|
||||
} = require(
|
||||
"../../utils/permissions"
|
||||
);
|
||||
|
||||
|
||||
module.exports = {
|
||||
data:
|
||||
new SlashCommandBuilder()
|
||||
@@ -22,9 +28,6 @@ module.exports = {
|
||||
.setDescription(
|
||||
"Administrative profile maintenance tools"
|
||||
)
|
||||
.setDefaultMemberPermissions(
|
||||
PermissionFlagsBits.Administrator
|
||||
)
|
||||
.addSubcommand(
|
||||
subcommand =>
|
||||
subcommand
|
||||
@@ -69,14 +72,32 @@ module.exports = {
|
||||
async execute(
|
||||
interaction
|
||||
) {
|
||||
/*
|
||||
* Allows:
|
||||
*
|
||||
* - Discord Administrators
|
||||
* - BOT_AUTHOR_ID from .env
|
||||
*/
|
||||
|
||||
if (
|
||||
!await requireAdminAccess(
|
||||
interaction
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const subcommand =
|
||||
interaction.options.getSubcommand();
|
||||
|
||||
|
||||
await interaction.deferReply({
|
||||
flags:
|
||||
MessageFlags.Ephemeral
|
||||
});
|
||||
|
||||
|
||||
if (
|
||||
subcommand ===
|
||||
"reconcile"
|
||||
@@ -86,21 +107,26 @@ module.exports = {
|
||||
"user"
|
||||
);
|
||||
|
||||
|
||||
const dryRun =
|
||||
interaction.options.getBoolean(
|
||||
"dry-run"
|
||||
) ?? false;
|
||||
|
||||
|
||||
if (user) {
|
||||
const result =
|
||||
await reconcileUserAchievements({
|
||||
guildId:
|
||||
interaction.guildId,
|
||||
|
||||
userId:
|
||||
user.id,
|
||||
|
||||
dryRun
|
||||
});
|
||||
|
||||
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
@@ -114,24 +140,30 @@ module.exports = {
|
||||
{
|
||||
name:
|
||||
"New unlocks",
|
||||
|
||||
value:
|
||||
`**${result.unlocked}**`,
|
||||
|
||||
inline:
|
||||
true
|
||||
},
|
||||
{
|
||||
name:
|
||||
"Definitions checked",
|
||||
|
||||
value:
|
||||
`**${result.checked}**`,
|
||||
|
||||
inline:
|
||||
true
|
||||
},
|
||||
{
|
||||
name:
|
||||
"Event-only skipped",
|
||||
|
||||
value:
|
||||
`**${result.skippedEventOnly}**`,
|
||||
|
||||
inline:
|
||||
true
|
||||
}
|
||||
@@ -140,13 +172,16 @@ module.exports = {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const summary =
|
||||
await reconcileAllAchievements({
|
||||
guildId:
|
||||
interaction.guildId,
|
||||
|
||||
dryRun
|
||||
});
|
||||
|
||||
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
@@ -160,24 +195,30 @@ module.exports = {
|
||||
{
|
||||
name:
|
||||
"Users",
|
||||
|
||||
value:
|
||||
`**${summary.users}**`,
|
||||
|
||||
inline:
|
||||
true
|
||||
},
|
||||
{
|
||||
name:
|
||||
"New unlocks",
|
||||
|
||||
value:
|
||||
`**${summary.unlocked}**`,
|
||||
|
||||
inline:
|
||||
true
|
||||
},
|
||||
{
|
||||
name:
|
||||
"Event-only checks skipped",
|
||||
|
||||
value:
|
||||
`**${summary.skippedEventOnly}**`,
|
||||
|
||||
inline:
|
||||
true
|
||||
}
|
||||
@@ -186,15 +227,23 @@ module.exports = {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const audit =
|
||||
await auditProfileData(
|
||||
interaction.guildId
|
||||
);
|
||||
|
||||
|
||||
const issuePreview =
|
||||
audit.issues
|
||||
.slice(0, 10)
|
||||
.join("\n");
|
||||
.slice(
|
||||
0,
|
||||
10
|
||||
)
|
||||
.join(
|
||||
"\n"
|
||||
);
|
||||
|
||||
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
@@ -206,48 +255,60 @@ module.exports = {
|
||||
{
|
||||
name:
|
||||
"Stats documents",
|
||||
|
||||
value:
|
||||
`**${audit.statDocuments}**`,
|
||||
|
||||
inline:
|
||||
true
|
||||
},
|
||||
{
|
||||
name:
|
||||
"Achievement documents",
|
||||
|
||||
value:
|
||||
`**${audit.achievementDocuments}**`,
|
||||
|
||||
inline:
|
||||
true
|
||||
},
|
||||
{
|
||||
name:
|
||||
"Active achievements",
|
||||
|
||||
value:
|
||||
`**${audit.activeAchievements}**`,
|
||||
|
||||
inline:
|
||||
true
|
||||
},
|
||||
{
|
||||
name:
|
||||
"Global badges",
|
||||
|
||||
value:
|
||||
`**${audit.badgeDocuments}** (${audit.claimedBadges} claimed)`,
|
||||
|
||||
inline:
|
||||
true
|
||||
},
|
||||
{
|
||||
name:
|
||||
"Orphan achievement docs",
|
||||
|
||||
value:
|
||||
`**${audit.orphanAchievementDocs}**`,
|
||||
|
||||
inline:
|
||||
true
|
||||
},
|
||||
{
|
||||
name:
|
||||
"Issues",
|
||||
|
||||
value:
|
||||
`**${audit.issueCount}**`,
|
||||
|
||||
inline:
|
||||
true
|
||||
}
|
||||
|
||||
@@ -24,30 +24,30 @@ const HEIST_COOLDOWN =
|
||||
|
||||
const HEIST_METHODS = {
|
||||
Sneak: {
|
||||
baseChance: 0.65,
|
||||
baseChance: 0.60,
|
||||
minStealPercent: 0.01,
|
||||
maxStealPercent: 0.03,
|
||||
maxStealAmount: 1500,
|
||||
maxStealAmount: 5000,
|
||||
minPenaltyPercent: 0.01,
|
||||
maxPenaltyPercent: 0.04,
|
||||
maxPenaltyAmount: 750
|
||||
},
|
||||
|
||||
Hack: {
|
||||
baseChance: 0.45,
|
||||
baseChance: 0.40,
|
||||
minStealPercent: 0.03,
|
||||
maxStealPercent: 0.06,
|
||||
maxStealAmount: 3500,
|
||||
maxStealAmount: 10000,
|
||||
minPenaltyPercent: 0.03,
|
||||
maxPenaltyPercent: 0.07,
|
||||
maxPenaltyAmount: 2000
|
||||
},
|
||||
|
||||
Smash: {
|
||||
baseChance: 0.25,
|
||||
baseChance: 0.20,
|
||||
minStealPercent: 0.06,
|
||||
maxStealPercent: 0.12,
|
||||
maxStealAmount: 7500,
|
||||
maxStealAmount: 15000,
|
||||
minPenaltyPercent: 0.06,
|
||||
maxPenaltyPercent: 0.12,
|
||||
maxPenaltyAmount: 5000
|
||||
|
||||
@@ -339,7 +339,10 @@ async function runRouletteBack(
|
||||
return showRouletteChoices(
|
||||
interaction,
|
||||
bet,
|
||||
false
|
||||
false,
|
||||
"run_it_back",
|
||||
previousAllIn,
|
||||
bet
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ function buildMainMenuButtons() {
|
||||
.setCustomId("crumbSaucerBlackjack")
|
||||
.setLabel("Blackjack")
|
||||
.setEmoji("🃏")
|
||||
.setStyle(ButtonStyle.Success),
|
||||
.setStyle(ButtonStyle.Primary),
|
||||
|
||||
new ButtonBuilder()
|
||||
.setCustomId("crumbSaucerRoulette")
|
||||
@@ -126,19 +126,22 @@ function buildMainMenuButtons() {
|
||||
.setCustomId("crumbSaucerCollectMenu")
|
||||
.setLabel("Cookie Bakery")
|
||||
.setEmoji("🍪")
|
||||
.setStyle(ButtonStyle.Success),
|
||||
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId("crumbSaucerBankruptcy")
|
||||
.setLabel("Bankruptcy")
|
||||
.setEmoji("🛟")
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
|
||||
new ButtonBuilder()
|
||||
.setCustomId("crumbSaucerProfile")
|
||||
.setLabel("Profile")
|
||||
.setEmoji("👤")
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId("sessionClose")
|
||||
.setLabel("Close")
|
||||
.setEmoji("✖️")
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setStyle(ButtonStyle.Danger)
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
+35
-148
@@ -1,118 +1,46 @@
|
||||
const {
|
||||
PermissionFlagsBits,
|
||||
MessageFlags
|
||||
} = require(
|
||||
"discord.js"
|
||||
);
|
||||
} = 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
|
||||
);
|
||||
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
|
||||
)
|
||||
return Boolean(
|
||||
authorId &&
|
||||
userId &&
|
||||
String(userId) === String(authorId)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* ============================================================
|
||||
* GUILD ADMINISTRATOR
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
function isAdministrator(
|
||||
interaction
|
||||
) {
|
||||
if (
|
||||
!interaction
|
||||
return (
|
||||
interaction
|
||||
?.memberPermissions
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
return interaction
|
||||
.memberPermissions
|
||||
.has(
|
||||
?.has(
|
||||
PermissionFlagsBits.Administrator
|
||||
)
|
||||
??
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* ============================================================
|
||||
* 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
|
||||
@@ -125,57 +53,6 @@ function canUseAdminCommands(
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* ============================================================
|
||||
* 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
|
||||
) {
|
||||
@@ -188,29 +65,39 @@ async function requireAdminAccess(
|
||||
}
|
||||
|
||||
|
||||
await denyAdminAccess(
|
||||
interaction
|
||||
const payload = {
|
||||
content:
|
||||
"You don't have permission to use this command.",
|
||||
|
||||
flags:
|
||||
MessageFlags.Ephemeral
|
||||
};
|
||||
|
||||
|
||||
if (
|
||||
interaction.replied ||
|
||||
interaction.deferred
|
||||
) {
|
||||
await interaction.followUp(
|
||||
payload
|
||||
);
|
||||
}
|
||||
|
||||
else {
|
||||
await interaction.reply(
|
||||
payload
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* ============================================================
|
||||
* EXPORTS
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
getBotAuthorId,
|
||||
|
||||
isBotAuthor,
|
||||
isAdministrator,
|
||||
|
||||
canUseAdminCommands,
|
||||
requireAdminAccess,
|
||||
|
||||
denyAdminAccess
|
||||
requireAdminAccess
|
||||
};
|
||||
@@ -18,6 +18,352 @@ const COLLECTIONS = {
|
||||
achievementConfig: "config_achievements"
|
||||
};
|
||||
|
||||
const PROFILE_TIMEZONE =
|
||||
process.env.PROFILE_TIMEZONE ??
|
||||
"Europe/Berlin";
|
||||
|
||||
const WEEKDAYS = [
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
"sunday"
|
||||
];
|
||||
function getActivityDateContext(
|
||||
date = new Date()
|
||||
) {
|
||||
const formatter =
|
||||
new Intl.DateTimeFormat(
|
||||
"en-CA",
|
||||
{
|
||||
timeZone:
|
||||
PROFILE_TIMEZONE,
|
||||
|
||||
year:
|
||||
"numeric",
|
||||
|
||||
month:
|
||||
"2-digit",
|
||||
|
||||
day:
|
||||
"2-digit",
|
||||
|
||||
weekday:
|
||||
"long",
|
||||
|
||||
hour:
|
||||
"2-digit",
|
||||
|
||||
hourCycle:
|
||||
"h23"
|
||||
}
|
||||
);
|
||||
|
||||
const parts =
|
||||
Object.fromEntries(
|
||||
formatter
|
||||
.formatToParts(date)
|
||||
.filter(
|
||||
part =>
|
||||
part.type !==
|
||||
"literal"
|
||||
)
|
||||
.map(
|
||||
part => [
|
||||
part.type,
|
||||
part.value
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
const dateKey =
|
||||
`${parts.year}-${parts.month}-${parts.day}`;
|
||||
|
||||
const weekday =
|
||||
String(
|
||||
parts.weekday
|
||||
).toLowerCase();
|
||||
|
||||
const hour =
|
||||
Number(
|
||||
parts.hour
|
||||
);
|
||||
|
||||
return {
|
||||
dateKey,
|
||||
yesterdayKey:
|
||||
getPreviousDateKey(
|
||||
dateKey
|
||||
),
|
||||
|
||||
weekday,
|
||||
hour,
|
||||
|
||||
weekKey:
|
||||
getIsoWeekKey(
|
||||
dateKey
|
||||
),
|
||||
|
||||
timeBucket:
|
||||
getTimeBucket(
|
||||
hour
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function getPreviousDateKey(
|
||||
dateKey
|
||||
) {
|
||||
const date =
|
||||
new Date(
|
||||
`${dateKey}T00:00:00Z`
|
||||
);
|
||||
|
||||
date.setUTCDate(
|
||||
date.getUTCDate() - 1
|
||||
);
|
||||
|
||||
return date
|
||||
.toISOString()
|
||||
.slice(
|
||||
0,
|
||||
10
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function getIsoWeekKey(
|
||||
dateKey
|
||||
) {
|
||||
const date =
|
||||
new Date(
|
||||
`${dateKey}T00:00:00Z`
|
||||
);
|
||||
|
||||
const day =
|
||||
date.getUTCDay() || 7;
|
||||
|
||||
date.setUTCDate(
|
||||
date.getUTCDate() +
|
||||
4 -
|
||||
day
|
||||
);
|
||||
|
||||
const year =
|
||||
date.getUTCFullYear();
|
||||
|
||||
const yearStart =
|
||||
new Date(
|
||||
Date.UTC(
|
||||
year,
|
||||
0,
|
||||
1
|
||||
)
|
||||
);
|
||||
|
||||
const week =
|
||||
Math.ceil(
|
||||
(
|
||||
(
|
||||
date -
|
||||
yearStart
|
||||
)
|
||||
/
|
||||
86400000
|
||||
+
|
||||
1
|
||||
)
|
||||
/
|
||||
7
|
||||
);
|
||||
|
||||
return (
|
||||
`${year}-W${String(week).padStart(2, "0")}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function getTimeBucket(
|
||||
hour
|
||||
) {
|
||||
if (
|
||||
hour >= 0 &&
|
||||
hour <= 5
|
||||
) {
|
||||
return "overnight";
|
||||
}
|
||||
|
||||
if (
|
||||
hour >= 6 &&
|
||||
hour <= 11
|
||||
) {
|
||||
return "morning";
|
||||
}
|
||||
|
||||
if (
|
||||
hour >= 12 &&
|
||||
hour <= 17
|
||||
) {
|
||||
return "afternoon";
|
||||
}
|
||||
|
||||
return "evening";
|
||||
}
|
||||
|
||||
async function trackGameActivity(
|
||||
userId,
|
||||
guildId
|
||||
) {
|
||||
const { stats } =
|
||||
getProfileCollections();
|
||||
|
||||
const {
|
||||
dateKey,
|
||||
yesterdayKey,
|
||||
weekday,
|
||||
weekKey,
|
||||
timeBucket
|
||||
} =
|
||||
getActivityDateContext();
|
||||
|
||||
|
||||
/*
|
||||
* Every completed game increments its weekday
|
||||
* and time-bucket counters.
|
||||
*/
|
||||
|
||||
await stats.updateOne(
|
||||
{
|
||||
userId,
|
||||
guildId
|
||||
},
|
||||
{
|
||||
$inc: {
|
||||
[`activity.gamesByWeekday.${weekday}`]:
|
||||
1,
|
||||
|
||||
[`activity.gamesByTimeBucket.${timeBucket}`]:
|
||||
1
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* Calendar-day state is only updated once per
|
||||
* player per local calendar day.
|
||||
*
|
||||
* The filter also protects against two games
|
||||
* resolving at nearly the same moment.
|
||||
*/
|
||||
|
||||
await stats.updateOne(
|
||||
{
|
||||
userId,
|
||||
guildId,
|
||||
|
||||
"activity.lastGameDate": {
|
||||
$ne:
|
||||
dateKey
|
||||
}
|
||||
},
|
||||
[
|
||||
{
|
||||
$set: {
|
||||
"activity.currentDailyStreak": {
|
||||
$cond: [
|
||||
{
|
||||
$eq: [
|
||||
"$activity.lastGameDate",
|
||||
yesterdayKey
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
$add: [
|
||||
{
|
||||
$ifNull: [
|
||||
"$activity.currentDailyStreak",
|
||||
0
|
||||
]
|
||||
},
|
||||
1
|
||||
]
|
||||
},
|
||||
|
||||
1
|
||||
]
|
||||
},
|
||||
|
||||
"activity.currentWeekDays": {
|
||||
$cond: [
|
||||
{
|
||||
$eq: [
|
||||
"$activity.currentWeekKey",
|
||||
weekKey
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
$setUnion: [
|
||||
{
|
||||
$ifNull: [
|
||||
"$activity.currentWeekDays",
|
||||
[]
|
||||
]
|
||||
},
|
||||
|
||||
[
|
||||
weekday
|
||||
]
|
||||
]
|
||||
},
|
||||
|
||||
[
|
||||
weekday
|
||||
]
|
||||
]
|
||||
},
|
||||
|
||||
"activity.currentWeekKey":
|
||||
weekKey,
|
||||
|
||||
"activity.lastActiveDate":
|
||||
dateKey,
|
||||
|
||||
"activity.lastGameDate":
|
||||
dateKey
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
$set: {
|
||||
"activity.bestDailyStreak": {
|
||||
$max: [
|
||||
{
|
||||
$ifNull: [
|
||||
"$activity.bestDailyStreak",
|
||||
0
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
$ifNull: [
|
||||
"$activity.currentDailyStreak",
|
||||
0
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
function getProfileCollections() {
|
||||
if (!DATABASE_NAME) {
|
||||
throw new Error("M_DB is not configured in .env");
|
||||
@@ -142,7 +488,37 @@ function getDefaultStats(userId, guildId) {
|
||||
|
||||
special: {
|
||||
allInRunItBack: 0
|
||||
},
|
||||
|
||||
activity: {
|
||||
currentDailyStreak: 0,
|
||||
bestDailyStreak: 0,
|
||||
|
||||
lastActiveDate: null,
|
||||
lastGameDate: null,
|
||||
|
||||
currentWeekKey: null,
|
||||
currentWeekDays: [],
|
||||
|
||||
gamesByWeekday: {
|
||||
monday: 0,
|
||||
tuesday: 0,
|
||||
wednesday: 0,
|
||||
thursday: 0,
|
||||
friday: 0,
|
||||
saturday: 0,
|
||||
sunday: 0
|
||||
},
|
||||
|
||||
gamesByTimeBucket: {
|
||||
overnight: 0,
|
||||
morning: 0,
|
||||
afternoon: 0,
|
||||
evening: 0
|
||||
}
|
||||
},
|
||||
|
||||
profileSchemaVersion: 2
|
||||
};
|
||||
}
|
||||
|
||||
@@ -293,11 +669,17 @@ async function trackProfileEvent(
|
||||
break;
|
||||
|
||||
case "game_result":
|
||||
await trackGameActivity(
|
||||
userId,
|
||||
guildId
|
||||
);
|
||||
|
||||
await trackGameResult(
|
||||
userId,
|
||||
guildId,
|
||||
event
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case "game_start":
|
||||
@@ -1391,6 +1773,15 @@ async function buildStatsEmbed(
|
||||
const streaks =
|
||||
stats.streaks ?? {};
|
||||
|
||||
const activity =
|
||||
stats.activity ?? {};
|
||||
|
||||
const gamesByWeekday =
|
||||
activity.gamesByWeekday ?? {};
|
||||
|
||||
const gamesByTimeBucket =
|
||||
activity.gamesByTimeBucket ?? {};
|
||||
|
||||
return new EmbedBuilder()
|
||||
.setTitle(
|
||||
`📊 ${user.globalName ?? user.username}'s Stats`
|
||||
@@ -1430,6 +1821,23 @@ async function buildStatsEmbed(
|
||||
].join("\n"),
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "📅 Activity",
|
||||
|
||||
value: [
|
||||
`Daily streak: **${activity.currentDailyStreak ?? 0} days**`,
|
||||
`Best streak: **${activity.bestDailyStreak ?? 0} days**`,
|
||||
"",
|
||||
`Mon **${gamesByWeekday.monday ?? 0}** • Tue **${gamesByWeekday.tuesday ?? 0}** • Wed **${gamesByWeekday.wednesday ?? 0}**`,
|
||||
`Thu **${gamesByWeekday.thursday ?? 0}** • Fri **${gamesByWeekday.friday ?? 0}**`,
|
||||
`Sat **${gamesByWeekday.saturday ?? 0}** • Sun **${gamesByWeekday.sunday ?? 0}**`,
|
||||
"",
|
||||
`🌙 Overnight **${gamesByTimeBucket.overnight ?? 0}**`,
|
||||
`🌅 Morning **${gamesByTimeBucket.morning ?? 0}**`,
|
||||
`☀️ Afternoon **${gamesByTimeBucket.afternoon ?? 0}**`,
|
||||
`🌆 Evening **${gamesByTimeBucket.evening ?? 0}**`
|
||||
].join("\n")
|
||||
},
|
||||
{
|
||||
name: "🎮 Per Game",
|
||||
value:
|
||||
@@ -2238,6 +2646,9 @@ module.exports = {
|
||||
getDefaultStats,
|
||||
readStatValue,
|
||||
|
||||
getActivityDateContext,
|
||||
trackGameActivity,
|
||||
|
||||
buildProfileOverview,
|
||||
buildStatsEmbed,
|
||||
buildBadgesEmbed,
|
||||
|
||||
Reference in New Issue
Block a user