diff --git a/typescript/server/src/lib/folders/get-session-folder-raises.ts b/typescript/server/src/lib/folders/get-session-folder-raises.ts index b4d288d62..b6625f988 100644 --- a/typescript/server/src/lib/folders/get-session-folder-raises.ts +++ b/typescript/server/src/lib/folders/get-session-folder-raises.ts @@ -69,43 +69,62 @@ export async function GetSessionFolderRaises( } for (const [metric, conf] of Object.entries(enumMetrics)) { - if (!info.isNewScore) { + const enumIndexes = score.scoreData.enumIndexes; + const newIdx = enumIndexes?.[metric as keyof typeof enumIndexes]; + + if (newIdx === undefined) { + continue; + } + + // Index of this metric before this score. New scores had no previous + // PB, so they crossed every threshold from the bottom. + let oldIdx: number; + + if (info.isNewScore) { + oldIdx = -1; + } else { const delta = info.deltas[metric]; if (delta === undefined || delta <= 0) { continue; } + + oldIdx = newIdx - delta; } - const enumIndexes = score.scoreData.enumIndexes; - const idx = enumIndexes?.[metric as keyof typeof enumIndexes]; + const minIdx = conf.values.indexOf(conf.minimumRelevantValue); - if (idx === undefined) { - continue; - } + // This view is cumulative: reaching e.g. EX HARD CLEAR also counts as + // a HARD CLEAR (and CLEAR, ...). So this score raised every enum + // value-or-better bucket it crossed, i.e. every threshold in + // (oldIdx, newIdx]. Only surface thresholds above the + // minimum-relevant value (so we don't show NO PLAY / FAILED rows). + const startIdx = Math.max(oldIdx, minIdx) + 1; - if (idx <= conf.values.indexOf(conf.minimumRelevantValue)) { - continue; - } - - const valueRaw = (score.scoreData as Record)[metric]; - - if (typeof valueRaw !== "string") { + if (startIdx > newIdx) { continue; } const folderIds = await folderIdsForChartCached(score.chartID, chartFolderCache); - for (const folderId of folderIds) { - const key = bucketKey(folderId, metric, valueRaw); - let set = bucket.get(key); + for (let vi = startIdx; vi <= newIdx; vi++) { + const value = conf.values[vi]; - if (!set) { - set = new Set(); - bucket.set(key, set); + if (value === undefined) { + continue; } - set.add(score.chartID); + for (const folderId of folderIds) { + const key = bucketKey(folderId, metric, value); + let set = bucket.get(key); + + if (!set) { + set = new Set(); + bucket.set(key, set); + } + + set.add(score.chartID); + } } } } @@ -143,7 +162,10 @@ export async function GetSessionFolderRaises( continue; } - const previousCount = dist.enumDist[metric]?.[value] ?? 0; + // Cumulative: how many charts were already at this value-or-better before + // the session. Pairs with the cumulative raise buckets above so that + // `previousCount + raisedCharts.length` is the true value-or-better total. + const previousCount = dist.cumulativeEnumDist[metric]?.[value] ?? 0; out.push({ folder, diff --git a/typescript/server/src/server/router/api/v1/sessions/_sessionID/router.test.ts b/typescript/server/src/server/router/api/v1/sessions/_sessionID/router.test.ts index ab8b2fcc5..15bedcc5a 100644 --- a/typescript/server/src/server/router/api/v1/sessions/_sessionID/router.test.ts +++ b/typescript/server/src/server/router/api/v1/sessions/_sessionID/router.test.ts @@ -165,6 +165,53 @@ describe("GET /api/v1/sessions/:sessionID/folder-raises", () => { expect(typeof hit.type).toBe("string"); expect(typeof hit.value).toBe("string"); }); + + it("treats raises cumulatively (an EX HARD CLEAR is also a HARD CLEAR)", async () => { + const { sessionId } = await seedSessionFixture(); + const folderId = `F_folder_sess_cumulative_${sessionId}`; + + await DB.insertInto("folder") + .values({ + id: folderId, + legacy_id: folderId, + game: "iidx-sp", + inactive: false, + title: "Session Folder Cumulative Test", + slug: folderId, + where: `chart.id = '${CHART_PG}'`, + version_filter: null, + search_terms: [], + }) + .execute(); + + await DB.insertInto("folder_chart_lookup") + .values({ folder_id: folderId, chart_id: CHART_PG }) + .execute(); + + const res = await mockApi.get(`/api/v1/sessions/${sessionId}/folder-raises`); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + + const lampRows = ( + res.body.body as Array<{ + folder: { folderID: string }; + raisedCharts: Array; + type: string; + value: string; + }> + ).filter((r) => r.folder.folderID === folderId && r.type === "lamp"); + + // The seeded score is an EX HARD CLEAR, so it should raise the EX HARD + // CLEAR bucket *and* the HARD CLEAR (and CLEAR) value-or-better buckets. + const exhc = lampRows.find((r) => r.value === "EX HARD CLEAR"); + const hc = lampRows.find((r) => r.value === "HARD CLEAR"); + + expect(exhc).toBeDefined(); + expect(exhc?.raisedCharts).toContain(CHART_PG); + expect(hc).toBeDefined(); + expect(hc?.raisedCharts).toContain(CHART_PG); + }); }); describe("GET /api/v1/sessions/:sessionID/adjacent", () => {