snapshot rollout phase 1
This commit is contained in:
+412
-1
@@ -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