snapshot migration phase 5

This commit is contained in:
DeSqBlocki
2026-08-27 19:34:16 +02:00
parent 42fc848f8c
commit 9710fc65bf
2 changed files with 518 additions and 4 deletions
+166
View File
@@ -896,6 +896,172 @@ async function auditProfileData(
continue;
}
/*
* ----------------------------------------------------
* EVENT ACHIEVEMENTS
* ----------------------------------------------------
*/
if (
definition.measure ===
"event"
) {
if (
!definition.eventType ||
typeof definition.eventType !==
"string"
) {
issues.push(
`${definition.achievementId}: event achievement has no eventType.`
);
}
if (
!Array.isArray(
definition.eventConditions
)
||
definition.eventConditions.length ===
0
) {
issues.push(
`${definition.achievementId}: event achievement has no eventConditions.`
);
continue;
}
const validEventOperators = [
"eq",
"neq",
"gt",
"gte",
"lt",
"lte",
"in",
"not_in",
"truthy",
"falsy"
];
for (
const [
index,
condition
]
of definition
.eventConditions
.entries()
) {
if (
!condition ||
typeof condition !==
"object"
) {
issues.push(
`${definition.achievementId}: event condition ${index + 1} is invalid.`
);
continue;
}
if (
!condition.field
) {
issues.push(
`${definition.achievementId}: event condition ${index + 1} has no field.`
);
}
const operator =
String(
condition.operator ??
"eq"
).toLowerCase();
if (
!validEventOperators.includes(
operator
)
) {
issues.push(
`${definition.achievementId}: event condition ${index + 1} has unknown operator "${operator}".`
);
}
if (
[
"eq",
"neq",
"gt",
"gte",
"lt",
"lte",
"in",
"not_in"
].includes(
operator
)
&&
condition.value ===
undefined
) {
issues.push(
`${definition.achievementId}: event condition ${index + 1} using "${operator}" is missing value.`
);
}
if (
[
"in",
"not_in"
].includes(
operator
)
&&
!Array.isArray(
condition.value
)
) {
issues.push(
`${definition.achievementId}: event condition ${index + 1} using "${operator}" requires an array value.`
);
}
if (
[
"gt",
"gte",
"lt",
"lte"
].includes(
operator
)
&&
!Number.isFinite(
Number(
condition.value
)
)
) {
issues.push(
`${definition.achievementId}: event condition ${index + 1} has a non-numeric comparison value.`
);
}
}
continue;
}
/*
* ----------------------------------------------------
+352 -4
View File
@@ -706,6 +706,14 @@ async function trackProfileEvent(
updatedStats
);
await evaluateEventAchievements(
interaction,
userId,
event
);
await evaluateBadges(
interaction,
userId,
@@ -713,6 +721,7 @@ async function trackProfileEvent(
event
);
return updatedStats;
}
@@ -977,8 +986,12 @@ async function evaluateAchievements(
const normalDefinitions =
definitions.filter(
definition =>
definition.measure !==
"achievementPercentage"
![
"achievementPercentage",
"event"
].includes(
definition.measure
)
);
for (const definition of normalDefinitions) {
@@ -2837,6 +2850,9 @@ function isStatBackfillableAchievement(
if (
definition.measure ===
"achievementPercentage"
||
definition.measure ===
"event"
) {
return false;
}
@@ -2849,7 +2865,8 @@ function isStatBackfillableAchievement(
return (
Array.isArray(
definition.conditions
) &&
)
&&
definition.conditions.length > 0
);
}
@@ -2937,6 +2954,331 @@ function getCompositeProgress(
conditions.length
};
}
function buildEventContext(
interaction,
event
) {
const activity =
getActivityDateContext();
return {
...event,
localDate:
activity.dateKey,
localWeekday:
activity.weekday,
localHour:
activity.hour,
localWeekKey:
activity.weekKey,
localTimeBucket:
activity.timeBucket,
isWeekend:
activity.weekday ===
"saturday"
||
activity.weekday ===
"sunday",
isWeekday:
![
"saturday",
"sunday"
].includes(
activity.weekday
)
};
}
function readEventPath(
event,
path
) {
if (!path) {
return undefined;
}
return String(path)
.split(".")
.reduce(
(current, key) =>
current?.[key],
event
);
}
function evaluateEventCondition(
event,
condition
) {
if (
!condition ||
!condition.field
) {
return false;
}
const actual =
readEventPath(
event,
condition.field
);
const expected =
condition.value;
const operator =
String(
condition.operator ??
"eq"
).toLowerCase();
switch (operator) {
case "eq":
return (
actual ===
expected
);
case "neq":
return (
actual !==
expected
);
case "gt":
return (
Number(actual ?? 0) >
Number(expected ?? 0)
);
case "gte":
return (
Number(actual ?? 0) >=
Number(expected ?? 0)
);
case "lt":
return (
Number(actual ?? 0) <
Number(expected ?? 0)
);
case "lte":
return (
Number(actual ?? 0) <=
Number(expected ?? 0)
);
case "in":
return (
Array.isArray(
expected
)
&&
expected.includes(
actual
)
);
case "not_in":
return (
Array.isArray(
expected
)
&&
!expected.includes(
actual
)
);
case "truthy":
return Boolean(
actual
);
case "falsy":
return !Boolean(
actual
);
default:
return false;
}
}
function evaluateEventAchievement(
event,
definition
) {
const conditions =
Array.isArray(
definition.eventConditions
)
? definition.eventConditions
: [];
if (!conditions.length) {
return false;
}
const mode =
String(
definition.eventMode ??
"all"
).toLowerCase();
if (mode === "any") {
return conditions.some(
condition =>
evaluateEventCondition(
event,
condition
)
);
}
return conditions.every(
condition =>
evaluateEventCondition(
event,
condition
)
);
}
async function evaluateEventAchievements(
interaction,
userId,
event
) {
const guildId =
interaction.guildId;
if (
!guildId ||
!event?.type
) {
return;
}
const {
achievements,
achievementConfig
} =
getProfileCollections();
await ensureAchievementDocument(
userId,
guildId
);
const definitions =
await achievementConfig
.find({
active: {
$ne: false
},
measure:
"event"
})
.toArray();
if (!definitions.length) {
return;
}
const achievementDocument =
await achievements.findOne({
userId,
guildId
});
const unlocked =
achievementDocument
?.unlocked ?? {};
const eventContext =
buildEventContext(
interaction,
event
);
for (
const definition
of definitions
) {
if (
unlocked[
definition.achievementId
]
) {
continue;
}
/*
* Optional fast filter.
*
* This avoids evaluating Roulette conditions
* against a Cookie Bakery event, for example.
*/
if (
definition.eventType &&
definition.eventType !==
eventContext.type
) {
continue;
}
if (
!evaluateEventAchievement(
eventContext,
definition
)
) {
continue;
}
await unlockAchievement(
interaction,
userId,
guildId,
definition
);
}
}
module.exports = {
ensureProfileSystem,
@@ -2967,5 +3309,11 @@ module.exports = {
getProfileContext,
getProfileContextFromFooter,
getProfileTargetId,
requireProfileOwner
requireProfileOwner,
buildEventContext,
readEventPath,
evaluateEventCondition,
evaluateEventAchievement,
evaluateEventAchievements,
};