snapshot rollout phase 1

This commit is contained in:
DeSqBlocki
2026-08-27 18:52:56 +02:00
parent 6a519f3a2d
commit c2ac7737c0
8 changed files with 595 additions and 171 deletions
+412 -1
View File
@@ -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,