This repository has been archived on 2023-10-27. You can view files and clone it, but cannot push or open issues or pull requests.
Files
scoresaber-reloaded/src/workers/stats-worker.js
2023-10-17 23:41:42 +01:00

257 lines
6.8 KiB
JavaScript

import { expose } from "comlink";
import initDb from "../db/db";
import scoresRepository from "../db/repository/scores";
import rankedsRepository from "../db/repository/rankeds";
import eventBus from "../utils/broadcast-channel-pubsub";
import { convertArrayToObjectByKey } from "../utils/js";
import { diffColors } from "../utils/scoresaber/format";
import { getAccFromScore } from "../utils/scoresaber/song";
import {
getTotalPpFromSortedPps,
WEIGHT_COEFFICIENT,
} from "../utils/scoresaber/pp";
import makePendingPromisePool from "../utils/pending-promises";
import produce, { setAutoFreeze } from "immer";
import beatmapsEnhancer from "../stores/http/enhancers/common/beatmaps";
import accEnhancer from "../stores/http/enhancers/scores/acc";
let db = null;
let rankeds = null;
const resolvePromiseOrWaitForPending = makePendingPromisePool();
const getPlayerScores = async (playerId) =>
scoresRepository().getAllFromIndex("scores-playerId", playerId, true);
const getRankedsFromDb = async (refreshCache = false) => {
const dbRankeds = await rankedsRepository().getAll(refreshCache);
return dbRankeds ? convertArrayToObjectByKey(dbRankeds, "leaderboardId") : {};
};
const getRankeds = async (refreshCache = false) =>
resolvePromiseOrWaitForPending(`rankeds/${refreshCache}`, () =>
getRankedsFromDb()
);
async function init() {
if (db) return;
db = await initDb();
// setup immer.js
// WORKAROUND for immer.js esm (see https://github.com/immerjs/immer/issues/557)
self.process = { env: { NODE_ENV: "production" } };
setAutoFreeze(false);
rankeds = getRankeds();
eventBus.on("rankeds-changed", () => (rankeds = getRankeds(true)));
}
const getRankedScores = async (playerId, withStars = false) => {
const scores = await getPlayerScores(playerId);
if (!scores || !scores.length) return null;
let allRankeds = null;
if (withStars) {
allRankeds = await rankeds;
}
return withStars
? (
await Promise.all(
scores
.filter((score) => score?.score?.pp)
.map(async (score) => {
score = await produce(
await produce(score, (draft) => beatmapsEnhancer(draft, true)),
(draft) => accEnhancer(draft)
);
return {
...score,
stars: allRankeds[score?.leaderboardId]?.stars ?? null,
};
})
)
).filter((s) => s.stars)
: scores.filter((score) => score?.score?.pp);
};
const getPlayerRankedScoresWithStars = async (playerId) =>
getRankedScores(playerId, true);
const calcPlayerStats = async (playerId) => {
await init();
const rankedScores = await getRankedScores(playerId);
if (!rankedScores) return null;
const stats = rankedScores
.filter(
(score) =>
(score?.score?.score && score?.score?.maxScore) || score?.score?.acc
)
.reduce(
(cum, s) => {
const leaderboardId = s?.leaderboard?.leaderboardId;
const pp = s?.score?.pp;
const score = s?.score?.unmodifiedScore ?? s?.score?.score ?? 0;
const accFromScore = getAccFromScore({ ...s.score, leaderboardId });
const scoreAcc = s?.score?.acc;
if (!accFromScore && !scoreAcc) return cum;
let acc = accFromScore ? accFromScore : scoreAcc;
if (!acc || isNaN(acc)) return cum;
s.score.acc = acc;
cum.totalScore += score;
cum.totalAcc += acc;
if (cum.topAcc < acc) cum.topAcc = acc;
if (cum.topPp < pp) cum.topPp = pp;
cum.badges.forEach((badge) => {
if (
(!badge.min || badge.min <= acc) &&
(!badge.max || badge.max > acc)
)
badge.value++;
});
return cum;
},
{
playerId,
badges: [
{
label: "SS+",
min: 95,
max: null,
value: 0,
bgColor: diffColors.expertPlus,
},
{
label: "SS",
min: 90,
max: 95,
value: 0,
bgColor: diffColors.expert,
},
{ label: "S+", min: 85, max: 90, value: 0, bgColor: diffColors.hard },
{
label: "S",
min: 80,
max: 85,
value: 0,
bgColor: diffColors.normal,
},
{
label: "A",
min: null,
max: 80,
value: 0,
bgColor: diffColors.easy,
},
],
topAcc: 0,
topPp: 0,
totalAcc: 0,
totalScore: 0,
avgAcc: 0,
playCount: rankedScores.length,
medianAcc: 0,
stdDeviation: 0,
}
);
stats.medianAcc =
rankedScores.length > 1
? rankedScores.sort((a, b) => a.score.acc - b.score.acc)[
Math.ceil(rankedScores.length / 2)
].score.acc
: stats.totalAcc;
stats.avgAcc = stats.totalAcc / rankedScores.length;
stats.stdDeviation = Math.sqrt(
rankedScores.reduce(
(sum, s) => sum + Math.pow(stats.avgAcc - s.score.acc, 2),
0
) / rankedScores.length
);
delete stats.totalAcc;
eventBus.publish("player-stats-calculated", stats);
return stats;
};
const calcPpBoundary = async (playerId, expectedPp = 1) => {
const rankedScores = await getRankedScores(playerId);
if (!rankedScores) return null;
const calcRawPpAtIdx = (bottomScores, idx, expected) => {
const oldBottomPp = getTotalPpFromSortedPps(bottomScores, idx);
const newBottomPp = getTotalPpFromSortedPps(bottomScores, idx + 1);
// 0.965^idx * rawPpToFind = expected + oldBottomPp - newBottomPp;
// rawPpToFind = (expected + oldBottomPp - newBottomPp) / 0.965^idx;
return (
(expected + oldBottomPp - newBottomPp) / Math.pow(WEIGHT_COEFFICIENT, idx)
);
};
const rankedScorePps = rankedScores.map((s) => s.pp).sort((a, b) => b - a);
let idx = rankedScorePps.length - 1;
while (idx >= 0) {
const bottomSlice = rankedScorePps.slice(idx);
const bottomPp = getTotalPpFromSortedPps(bottomSlice, idx);
bottomSlice.unshift(rankedScorePps[idx]);
const modifiedBottomPp = getTotalPpFromSortedPps(bottomSlice, idx);
const diff = modifiedBottomPp - bottomPp;
if (diff > expectedPp) {
const ppBoundary = calcRawPpAtIdx(
rankedScorePps.slice(idx + 1),
idx + 1,
expectedPp
);
eventBus.publish("player-pp-boundary-calculated", {
playerId,
expectedPp,
ppBoundary,
});
return ppBoundary;
}
idx--;
}
const ppBoundary = calcRawPpAtIdx(rankedScorePps, 0, expectedPp);
eventBus.publish("player-pp-boundary-calculated", {
playerId,
expectedPp,
ppBoundary,
});
return ppBoundary;
};
const worker = {
init,
calcPlayerStats,
calcPpBoundary,
getPlayerRankedScoresWithStars,
};
expose(worker);