snapshot migration phase 5
This commit is contained in:
@@ -896,6 +896,172 @@ async function auditProfileData(
|
|||||||
continue;
|
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
@@ -706,6 +706,14 @@ async function trackProfileEvent(
|
|||||||
updatedStats
|
updatedStats
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
await evaluateEventAchievements(
|
||||||
|
interaction,
|
||||||
|
userId,
|
||||||
|
event
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
await evaluateBadges(
|
await evaluateBadges(
|
||||||
interaction,
|
interaction,
|
||||||
userId,
|
userId,
|
||||||
@@ -713,6 +721,7 @@ async function trackProfileEvent(
|
|||||||
event
|
event
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
return updatedStats;
|
return updatedStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -977,8 +986,12 @@ async function evaluateAchievements(
|
|||||||
const normalDefinitions =
|
const normalDefinitions =
|
||||||
definitions.filter(
|
definitions.filter(
|
||||||
definition =>
|
definition =>
|
||||||
definition.measure !==
|
![
|
||||||
"achievementPercentage"
|
"achievementPercentage",
|
||||||
|
"event"
|
||||||
|
].includes(
|
||||||
|
definition.measure
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const definition of normalDefinitions) {
|
for (const definition of normalDefinitions) {
|
||||||
@@ -2837,6 +2850,9 @@ function isStatBackfillableAchievement(
|
|||||||
if (
|
if (
|
||||||
definition.measure ===
|
definition.measure ===
|
||||||
"achievementPercentage"
|
"achievementPercentage"
|
||||||
|
||
|
||||||
|
definition.measure ===
|
||||||
|
"event"
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -2849,7 +2865,8 @@ function isStatBackfillableAchievement(
|
|||||||
return (
|
return (
|
||||||
Array.isArray(
|
Array.isArray(
|
||||||
definition.conditions
|
definition.conditions
|
||||||
) &&
|
)
|
||||||
|
&&
|
||||||
definition.conditions.length > 0
|
definition.conditions.length > 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2937,6 +2954,331 @@ function getCompositeProgress(
|
|||||||
conditions.length
|
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 = {
|
module.exports = {
|
||||||
ensureProfileSystem,
|
ensureProfileSystem,
|
||||||
@@ -2967,5 +3309,11 @@ module.exports = {
|
|||||||
getProfileContext,
|
getProfileContext,
|
||||||
getProfileContextFromFooter,
|
getProfileContextFromFooter,
|
||||||
getProfileTargetId,
|
getProfileTargetId,
|
||||||
requireProfileOwner
|
requireProfileOwner,
|
||||||
|
|
||||||
|
buildEventContext,
|
||||||
|
readEventPath,
|
||||||
|
evaluateEventCondition,
|
||||||
|
evaluateEventAchievement,
|
||||||
|
evaluateEventAchievements,
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user