2023-10-17 23:38:18 +01:00
|
|
|
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";
|
2023-10-17 21:42:37 +01:00
|
|
|
|
|
|
|
let db = null;
|
|
|
|
|
|
|
|
let rankeds = null;
|
|
|
|
|
|
|
|
const resolvePromiseOrWaitForPending = makePendingPromisePool();
|
|
|
|
|
2023-10-17 23:38:18 +01:00
|
|
|
const getPlayerScores = async (playerId) =>
|
|
|
|
scoresRepository().getAllFromIndex("scores-playerId", playerId, true);
|
2023-10-17 21:42:37 +01:00
|
|
|
const getRankedsFromDb = async (refreshCache = false) => {
|
2023-10-17 23:38:18 +01:00
|
|
|
const dbRankeds = await rankedsRepository().getAll(refreshCache);
|
2023-10-17 21:42:37 +01:00
|
|
|
|
2023-10-17 23:38:18 +01:00
|
|
|
return dbRankeds ? convertArrayToObjectByKey(dbRankeds, "leaderboardId") : {};
|
|
|
|
};
|
2023-10-17 21:42:37 +01:00
|
|
|
|
2023-10-17 23:38:18 +01:00
|
|
|
const getRankeds = async (refreshCache = false) =>
|
|
|
|
resolvePromiseOrWaitForPending(`rankeds/${refreshCache}`, () =>
|
2023-10-17 23:41:42 +01:00
|
|
|
getRankedsFromDb()
|
2023-10-17 23:38:18 +01:00
|
|
|
);
|
2023-10-17 21:42:37 +01:00
|
|
|
|
|
|
|
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)
|
2023-10-17 23:38:18 +01:00
|
|
|
self.process = { env: { NODE_ENV: "production" } };
|
2023-10-17 21:42:37 +01:00
|
|
|
setAutoFreeze(false);
|
|
|
|
|
|
|
|
rankeds = getRankeds();
|
|
|
|
|
2023-10-17 23:38:18 +01:00
|
|
|
eventBus.on("rankeds-changed", () => (rankeds = getRankeds(true)));
|
2023-10-17 21:42:37 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const getRankedScores = async (playerId, withStars = false) => {
|
2023-10-17 23:38:18 +01:00
|
|
|
const scores = await getPlayerScores(playerId);
|
2023-10-17 21:42:37 +01:00
|
|
|
|
|
|
|
if (!scores || !scores.length) return null;
|
|
|
|
|
|
|
|
let allRankeds = null;
|
|
|
|
if (withStars) {
|
|
|
|
allRankeds = await rankeds;
|
|
|
|
}
|
|
|
|
|
|
|
|
return withStars
|
2023-10-17 23:38:18 +01:00
|
|
|
? (
|
|
|
|
await Promise.all(
|
|
|
|
scores
|
|
|
|
.filter((score) => score?.score?.pp)
|
|
|
|
.map(async (score) => {
|
|
|
|
score = await produce(
|
|
|
|
await produce(score, (draft) => beatmapsEnhancer(draft, true)),
|
2023-10-17 23:41:42 +01:00
|
|
|
(draft) => accEnhancer(draft)
|
2023-10-17 23:38:18 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
return {
|
|
|
|
...score,
|
|
|
|
stars: allRankeds[score?.leaderboardId]?.stars ?? null,
|
|
|
|
};
|
2023-10-17 23:41:42 +01:00
|
|
|
})
|
2023-10-17 23:38:18 +01:00
|
|
|
)
|
|
|
|
).filter((s) => s.stars)
|
|
|
|
: scores.filter((score) => score?.score?.pp);
|
|
|
|
};
|
|
|
|
|
|
|
|
const getPlayerRankedScoresWithStars = async (playerId) =>
|
|
|
|
getRankedScores(playerId, true);
|
|
|
|
|
|
|
|
const calcPlayerStats = async (playerId) => {
|
2023-10-17 21:42:37 +01:00
|
|
|
await init();
|
|
|
|
|
|
|
|
const rankedScores = await getRankedScores(playerId);
|
|
|
|
if (!rankedScores) return null;
|
|
|
|
|
|
|
|
const stats = rankedScores
|
2023-10-17 23:38:18 +01:00
|
|
|
.filter(
|
|
|
|
(score) =>
|
2023-10-17 23:41:42 +01:00
|
|
|
(score?.score?.score && score?.score?.maxScore) || score?.score?.acc
|
2023-10-17 23:38:18 +01:00
|
|
|
)
|
|
|
|
.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,
|
2023-10-17 23:41:42 +01:00
|
|
|
}
|
2023-10-17 23:38:18 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
stats.medianAcc =
|
|
|
|
rankedScores.length > 1
|
|
|
|
? rankedScores.sort((a, b) => a.score.acc - b.score.acc)[
|
|
|
|
Math.ceil(rankedScores.length / 2)
|
|
|
|
].score.acc
|
|
|
|
: stats.totalAcc;
|
2023-10-17 21:42:37 +01:00
|
|
|
stats.avgAcc = stats.totalAcc / rankedScores.length;
|
2023-10-17 23:38:18 +01:00
|
|
|
stats.stdDeviation = Math.sqrt(
|
|
|
|
rankedScores.reduce(
|
|
|
|
(sum, s) => sum + Math.pow(stats.avgAcc - s.score.acc, 2),
|
2023-10-17 23:41:42 +01:00
|
|
|
0
|
|
|
|
) / rankedScores.length
|
2023-10-17 23:38:18 +01:00
|
|
|
);
|
2023-10-17 21:42:37 +01:00
|
|
|
|
|
|
|
delete stats.totalAcc;
|
|
|
|
|
2023-10-17 23:38:18 +01:00
|
|
|
eventBus.publish("player-stats-calculated", stats);
|
2023-10-17 21:42:37 +01:00
|
|
|
|
|
|
|
return stats;
|
2023-10-17 23:38:18 +01:00
|
|
|
};
|
2023-10-17 21:42:37 +01:00
|
|
|
|
|
|
|
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;
|
2023-10-17 23:38:18 +01:00
|
|
|
return (
|
|
|
|
(expected + oldBottomPp - newBottomPp) / Math.pow(WEIGHT_COEFFICIENT, idx)
|
|
|
|
);
|
|
|
|
};
|
2023-10-17 21:42:37 +01:00
|
|
|
|
2023-10-17 23:38:18 +01:00
|
|
|
const rankedScorePps = rankedScores.map((s) => s.pp).sort((a, b) => b - a);
|
2023-10-17 21:42:37 +01:00
|
|
|
|
|
|
|
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) {
|
2023-10-17 23:38:18 +01:00
|
|
|
const ppBoundary = calcRawPpAtIdx(
|
|
|
|
rankedScorePps.slice(idx + 1),
|
|
|
|
idx + 1,
|
2023-10-17 23:41:42 +01:00
|
|
|
expectedPp
|
2023-10-17 23:38:18 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
eventBus.publish("player-pp-boundary-calculated", {
|
|
|
|
playerId,
|
|
|
|
expectedPp,
|
|
|
|
ppBoundary,
|
|
|
|
});
|
2023-10-17 21:42:37 +01:00
|
|
|
|
|
|
|
return ppBoundary;
|
|
|
|
}
|
|
|
|
|
|
|
|
idx--;
|
|
|
|
}
|
|
|
|
|
|
|
|
const ppBoundary = calcRawPpAtIdx(rankedScorePps, 0, expectedPp);
|
|
|
|
|
2023-10-17 23:38:18 +01:00
|
|
|
eventBus.publish("player-pp-boundary-calculated", {
|
|
|
|
playerId,
|
|
|
|
expectedPp,
|
|
|
|
ppBoundary,
|
|
|
|
});
|
2023-10-17 21:42:37 +01:00
|
|
|
|
|
|
|
return ppBoundary;
|
2023-10-17 23:38:18 +01:00
|
|
|
};
|
2023-10-17 21:42:37 +01:00
|
|
|
|
|
|
|
const worker = {
|
|
|
|
init,
|
|
|
|
calcPlayerStats,
|
|
|
|
calcPpBoundary,
|
2023-10-17 23:38:18 +01:00
|
|
|
getPlayerRankedScoresWithStars,
|
|
|
|
};
|
2023-10-17 21:42:37 +01:00
|
|
|
|
2023-10-17 23:38:18 +01:00
|
|
|
expose(worker);
|