fork from scoresaber-reloaded

This commit is contained in:
Lee
2023-10-17 21:42:37 +01:00
commit cc884eec07
229 changed files with 31236 additions and 0 deletions

187
src/workers/stats-worker.js Normal file
View File

@ -0,0 +1,187 @@
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);