milestone support in one

This commit is contained in:
zkldi
2021-05-09 20:50:28 +01:00
parent 5856a1aaa5
commit 34bd38252f
4 changed files with 220 additions and 4 deletions
+3 -3
View File
@@ -62,7 +62,7 @@ dependencies:
express-async-errors: 3.1.1_express@4.17.1
express-session: 1.17.1
fast-json-stable-hash: 1.0.2
kamaitachi-common: github.com/zkldi/Kamaitachi-common/67f10b22a38cee3c270d74d2091466aef45c2318
kamaitachi-common: github.com/zkldi/Kamaitachi-common/f10a13f67d186ca9caa474092270f0995ff6c24d
mongodb: 3.6.6
monk: 7.3.4
multer: 1.4.2
@@ -4638,8 +4638,8 @@ packages:
'@types/yoga-layout': 1.9.2
dev: true
github.com/zkldi/Kamaitachi-common/67f10b22a38cee3c270d74d2091466aef45c2318:
resolution: {tarball: https://codeload.github.com/zkldi/Kamaitachi-common/tar.gz/67f10b22a38cee3c270d74d2091466aef45c2318}
github.com/zkldi/Kamaitachi-common/f10a13f67d186ca9caa474092270f0995ff6c24d:
resolution: {tarball: https://codeload.github.com/zkldi/Kamaitachi-common/tar.gz/f10a13f67d186ca9caa474092270f0995ff6c24d}
name: kamaitachi-common
version: 0.1.0
dependencies:
+82
View File
@@ -0,0 +1,82 @@
import {
MilestoneDocument,
UserMilestoneDocument,
integer,
GoalImportInfo,
} from "kamaitachi-common";
/**
* Processes and updates a user's milestones from their Goal Import Info (i.e. what is returned
* about goals from imports)
*/
export function ProcessMilestoneFromGII(
milestone: MilestoneDocument,
gii: Map<string, GoalImportInfo["new"]>
) {
let goalIDs = GetGoalIDsFromMilestone(milestone);
let progress = 0;
for (const goalID of goalIDs) {
let userInfo = gii.get(goalID);
if (!userInfo) {
continue;
}
if (!userInfo.achieved) {
continue;
}
progress++;
}
let outOf = CalculateMilestoneOutOf(milestone, goalIDs);
// milestone achieved!
if (progress >= outOf) {
return { achieved: true, progress };
}
return { achieved: false, progress };
}
/**
* Retrieves the goalID documents in a single array from the
* nested structure of milestones.
*/
function GetGoalIDsFromMilestone(milestone: MilestoneDocument) {
// this sucks - maybe a nicer way to do this, because nested
// maps are just ugly
return milestone.milestoneData.map((e) => e.goals.map((e) => e.goalID)).flat(1);
}
/**
* Work out how many goals need to be achieved for this
* milestone to be considered completed.
*/
function CalculateMilestoneOutOf(milestone: MilestoneDocument, goalIDs: string[]): integer {
if (milestone.criteria.type === "all") {
return goalIDs.length;
} else if (milestone.criteria.type === "abs") {
if (milestone.criteria.value === null) {
throw new Error(
`Invalid milestone ${milestone.milestoneID} - abs and null are not compatible.`
);
}
return milestone.criteria.value!;
} else if (milestone.criteria.type === "proportion") {
if (milestone.criteria.value === null) {
throw new Error(
`Invalid milestone ${milestone.milestoneID} - proportion and null are not compatible.`
);
}
return Math.floor(milestone.criteria.value * goalIDs.length);
}
throw new Error(
`Invalid milestone.criteria.type of ${milestone.criteria.type} -- milestoneID ${milestone.milestoneID}`
);
}
@@ -0,0 +1,128 @@
import { KtLogger } from "../../../types";
import {
integer,
Game,
Playtypes,
GoalImportInfo,
UserMilestoneDocument,
MilestoneImportInfo,
} from "kamaitachi-common";
import db from "../../../db/db";
import { ProcessMilestoneFromGII } from "../../../core/milestone-core";
import { BulkWriteUpdateOneOperation } from "mongodb";
export async function UpdateUsersMilestones(
importGoalInfo: GoalImportInfo[],
game: Game,
playtypes: Playtypes[Game][],
userID: integer,
logger: KtLogger
) {
let userGoalInfoMap: Map<string, GoalImportInfo["new"]> = new Map();
let goalIDs = [];
for (const e of importGoalInfo) {
userGoalInfoMap.set(e.goalID, e.new);
goalIDs.push(e.goalID);
}
let { milestones, userMilestones } = await GetRelevantMilestones(
goalIDs,
game,
playtypes,
userID,
logger
);
// create a map here to avoid linear searching when
// co-iterating
let userMilestoneMap = new Map();
for (const um of userMilestones) {
userMilestoneMap.set(um.milestoneID, um);
}
let importGoalMap = new Map();
for (const ig of importGoalInfo) {
importGoalMap.set(ig.goalID, ig.new);
}
let bwrite: BulkWriteUpdateOneOperation<UserMilestoneDocument>[] = [];
let importMilestoneInfo: MilestoneImportInfo[] = [];
for (const milestone of milestones) {
let { achieved, progress } = ProcessMilestoneFromGII(milestone, importGoalMap);
let userMilestone = userMilestoneMap.get(milestone.milestoneID);
if (!userMilestone) {
logger.severe(
`Invalid state achieved in milestone processing - processed milestone that user did not have? ${milestone.milestoneID}`
);
throw new Error("Invalid state achieved in milestone processing.");
}
bwrite.push({
updateOne: {
filter: { milestoneID: milestone.milestoneID },
update: {
$set: {
achieved,
progress,
},
},
},
});
if (progress !== userMilestone.progress) {
importMilestoneInfo.push({
milestoneID: userMilestone.milestoneID,
old: {
progress: userMilestone.progress,
achieved: userMilestone.achieved,
},
new: {
progress,
achieved,
},
});
}
if (achieved && !userMilestone.achieved) {
// @todo emit some sort of event
}
}
if (bwrite.length !== 0) {
await db["user-milestones"].bulkWrite(bwrite, { ordered: false });
}
return importMilestoneInfo;
}
async function GetRelevantMilestones(
goalIDs: string[],
game: Game,
playtypes: Playtypes[Game][],
userID: integer,
logger: KtLogger
) {
let userMilestones = await db["user-milestones"].find({
game,
playtype: { $in: playtypes },
userID,
});
logger.debug(`Found ${userMilestones.length} user-milestones.`);
let milestones = await db.milestones.find({
milestoneID: { $in: userMilestones.map((e) => e.milestoneID) },
"milestoneData.goals.goalID": { $in: goalIDs },
});
logger.debug(`Found ${milestones.length} relevant milestones.`);
return { userMilestones, milestones };
}
@@ -16,6 +16,7 @@ import { ProcessPBs } from "./pb/process-pbs";
import { UpdateUsersGamePlaytypeStats } from "./user-game-stats/update-ugs";
import db from "../../db/db";
import { UpdateUsersGoals } from "./goals/goals";
import { UpdateUsersMilestones } from "./milestones/milestones";
export default async function ScoreImportMain<D, C>(
user: PublicUserDocument,
@@ -91,7 +92,11 @@ export default async function ScoreImportMain<D, C>(
// Evaluate and update the users goals. This returns information about goals that have changed.
let goalInfo = await UpdateUsersGoals(game, user.id, chartIDs, logger);
// --- 8. Finalise Import Document ---
// --- 8. Milestones ---
// Evaluate and update the users milestones. This returns...
let milestoneInfo = await UpdateUsersMilestones(goalInfo, game, playtypes, user.id, logger);
// --- 9. Finalise Import Document ---
// Create and Save an import document to the database, and finish everything up!
const ImportDocument: ImportDocument = {
importType,
@@ -105,6 +110,7 @@ export default async function ScoreImportMain<D, C>(
userID: user.id,
classDeltas,
goalInfo,
milestoneInfo,
userIntent,
};