From 2b37c4e793010242d3559f9e00196d2ee4b2c870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Tue, 11 Mar 2025 19:11:22 -0400 Subject: [PATCH] cleanup --- .../tools/achievements/achievement-manager.ts | 48 ++++++++++++ .../src/tools/achievements/event-processor.ts | 73 ++++--------------- libs/common/src/tools/achievements/util.ts | 8 ++ 3 files changed, 70 insertions(+), 59 deletions(-) create mode 100644 libs/common/src/tools/achievements/achievement-manager.ts create mode 100644 libs/common/src/tools/achievements/util.ts diff --git a/libs/common/src/tools/achievements/achievement-manager.ts b/libs/common/src/tools/achievements/achievement-manager.ts new file mode 100644 index 00000000000..6da26650bdb --- /dev/null +++ b/libs/common/src/tools/achievements/achievement-manager.ts @@ -0,0 +1,48 @@ +import { Observable, OperatorFunction, map, pipe, withLatestFrom } from "rxjs"; + +import { isEarnedEvent } from "./meta"; +import { AchievementEvent, AchievementValidator } from "./types"; +import { mapProgressByName } from "./util"; + +// computes the list of live achievements; those whose trigger conditions +// aren't met are excluded from the active set +function active( + status$: Observable, +): OperatorFunction { + return pipe( + withLatestFrom(status$), + map(([monitors, log]) => { + // partition the log into progress and earned achievements + const progressByName = mapProgressByName(log); + const earnedByName = new Set( + log.filter((e) => isEarnedEvent(e)).map((e) => e.achievement.name), + ); + + // compute list of active achievements + const active = monitors.filter((m) => { + // 🧠 the filters could be lifted into a function argument & delivered + // as a `Map bool> + + if (m.trigger === "once") { + // monitor disabled if already achieved + return !earnedByName.has(m.achievement); + } + + // monitor disabled if outside of threshold + const progress = (m.metric && progressByName.get(m.metric)) || 0; + if (progress > (m.trigger.high ?? Number.POSITIVE_INFINITY)) { + return false; + } else if (progress < (m.trigger.low ?? 0)) { + return false; + } + + // otherwise you're within the threshold, so the monitor is active + return true; + }); + + return active; + }), + ); +} + +export { active }; diff --git a/libs/common/src/tools/achievements/event-processor.ts b/libs/common/src/tools/achievements/event-processor.ts index b80156d127f..feb41b3b552 100644 --- a/libs/common/src/tools/achievements/event-processor.ts +++ b/libs/common/src/tools/achievements/event-processor.ts @@ -2,89 +2,43 @@ import { Observable, OperatorFunction, concatMap, from, map, pipe, withLatestFro import { EventFormat } from "../log/ecs-format"; +import { active } from "./achievement-manager"; import { achievementMonitors$, achievementsLocal$ as achievementsLog$, userActionIn$, } from "./inputs"; -import { isEarnedEvent, isProgressEvent } from "./meta"; import { AchievementEvent, AchievementValidator } from "./types"; - -function mapProgressByName(status: AchievementEvent[]) { - return new Map( - status.filter(isProgressEvent).map((e) => [e.achievement.name, e.achievement.value] as const), - ); -} - -// OPTIMIZATION: compute the list of active monitors from trigger criteria -function active( - status$: Observable, -): OperatorFunction { - return pipe( - withLatestFrom(status$), - map(([monitors, log]) => { - // partition the log into progress and earned achievements - const progressByName = mapProgressByName(log); - const earnedByName = new Set( - log.filter((e) => isEarnedEvent(e)).map((e) => e.achievement.name), - ); - - // compute list of active achievements - const active = monitors.filter((m) => { - // 🧠 the filters could be lifted into a function argument & delivered - // as a `Map bool> - - if (m.trigger === "once") { - // monitor disabled if already achieved - return !earnedByName.has(m.achievement); - } - - // monitor disabled if outside of threshold - const progress = (m.metric && progressByName.get(m.metric)) || 0; - if (progress > (m.trigger.high ?? Number.POSITIVE_INFINITY)) { - return false; - } else if (progress < (m.trigger.low ?? 0)) { - return false; - } - - // otherwise you're within the threshold, so the monitor is active - return true; - }); - - return active; - }), - ); -} +import { mapProgressByName } from "./util"; // the formal event processor function validate( - monitors$: Observable, - status$: Observable, + validators$: Observable, + captured$: Observable, ): OperatorFunction { return pipe( - withLatestFrom(monitors$), + withLatestFrom(validators$), map(([action, monitors]) => { // narrow the list of all live monitors to just those that may produce new logs const triggered = monitors.filter((m) => m.filter(action)); return [action, triggered] as const; }), - withLatestFrom(status$), - concatMap(([[action, monitors], status]) => { + withLatestFrom(captured$), + concatMap(([[action, validators], captured]) => { const results: AchievementEvent[] = []; + const progress = mapProgressByName(captured); - // process achievement monitors sequentially, accumulating result records - for (const monitor of monitors) { - const progress = mapProgressByName(status); - - const measured = monitor.measure(action, progress); + for (const validator of validators) { + const measured = validator.measure(action, progress); results.push(...measured); - // modify copy produced by filter to avoid reallocation + // update progress with the latest measurements for (const m of measured) { progress.set(m.achievement.name, m.achievement.value); } - results.push(...monitor.earn(measured, progress)); + const earned = validator.earn(measured, progress); + results.push(...earned); } // deliver results as a stream containing individual records to maintain @@ -94,6 +48,7 @@ function validate( ); } +// monitors are lazy until their trigger condition is met const liveMonitors$ = achievementMonitors$.pipe(active(achievementsLog$)); // pre-wired achievement stream; this is the prototype's host, and diff --git a/libs/common/src/tools/achievements/util.ts b/libs/common/src/tools/achievements/util.ts new file mode 100644 index 00000000000..0a9f364d7fc --- /dev/null +++ b/libs/common/src/tools/achievements/util.ts @@ -0,0 +1,8 @@ +import { isProgressEvent } from "./meta"; +import { AchievementEvent } from "./types"; + +export function mapProgressByName(status: AchievementEvent[]) { + return new Map( + status.filter(isProgressEvent).map((e) => [e.achievement.name, e.achievement.value] as const), + ); +}